Bug 1142515: implement updating a room with changed context information. r=Standard8
authorMike de Boer <mdeboer@mozilla.com>
Thu, 07 May 2015 11:38:57 +0200
changeset 242662 de2e38a20ccaedac1944a2269f53b3fd11aa312d
parent 242661 991020c5923b65230677917124cd827ead25cb68
child 242663 855bbeb7d41f3ece691a0030b059f9f43f8da776
push id12781
push usermdeboer@mozilla.com
push dateThu, 07 May 2015 09:42:46 +0000
treeherderfx-team@855bbeb7d41f [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1142515
milestone40.0a1
Bug 1142515: implement updating a room with changed context information. r=Standard8
browser/components/loop/content/js/panel.js
browser/components/loop/content/js/panel.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/modules/LoopRooms.jsm
browser/components/loop/standalone/content/l10n/en-US/loop.properties
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -431,17 +431,17 @@ loop.panel = (function(_, mozL10n) {
     }
   });
 
   var RoomEntryContextItem = React.createClass({displayName: "RoomEntryContextItem",
     mixins: [loop.shared.mixins.WindowCloseMixin],
 
     propTypes: {
       mozLoop: React.PropTypes.object.isRequired,
-      roomUrls: React.PropTypes.object
+      roomUrls: React.PropTypes.array
     },
 
     handleClick: function(event) {
       event.stopPropagation();
       event.preventDefault();
       this.props.mozLoop.openURL(event.currentTarget.href);
       this.closeWindow();
     },
--- a/browser/components/loop/content/js/panel.jsx
+++ b/browser/components/loop/content/js/panel.jsx
@@ -431,17 +431,17 @@ loop.panel = (function(_, mozL10n) {
     }
   });
 
   var RoomEntryContextItem = React.createClass({
     mixins: [loop.shared.mixins.WindowCloseMixin],
 
     propTypes: {
       mozLoop: React.PropTypes.object.isRequired,
-      roomUrls: React.PropTypes.object
+      roomUrls: React.PropTypes.array
     },
 
     handleClick: function(event) {
       event.stopPropagation();
       event.preventDefault();
       this.props.mozLoop.openURL(event.currentTarget.href);
       this.closeWindow();
     },
--- a/browser/components/loop/content/js/roomStore.js
+++ b/browser/components/loop/content/js/roomStore.js
@@ -86,19 +86,19 @@ loop.store = loop.store || {};
       "createRoomError",
       "copyRoomUrl",
       "deleteRoom",
       "deleteRoomError",
       "emailRoomUrl",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
-      "renameRoom",
-      "renameRoomError",
       "shareRoomUrl",
+      "updateRoomContext",
+      "updateRoomContextError",
       "updateRoomList"
     ],
 
     initialize: function(options) {
       if (!options.mozLoop) {
         throw new Error("Missing option mozLoop");
       }
       this._mozLoop = options.mozLoop;
@@ -464,35 +464,85 @@ loop.store = loop.store || {};
      *
      * @param {sharedActions.OpenRoom} actionData The action data.
      */
     openRoom: function(actionData) {
       this._mozLoop.rooms.open(actionData.roomToken);
     },
 
     /**
-     * Renames a room.
+     * Updates the context data attached to a room.
      *
-     * @param {sharedActions.RenameRoom} actionData
+     * @param {sharedActions.UpdateRoomContext} actionData
      */
-    renameRoom: function(actionData) {
-      var oldRoomName = this.getStoreState("roomName");
-      var newRoomName = actionData.newRoomName.trim();
+    updateRoomContext: function(actionData) {
+      this._mozLoop.rooms.get(actionData.roomToken, function(err, room) {
+        if (err) {
+          this.dispatchAction(new sharedActions.UpdateRoomContextError({
+            error: err
+          }));
+          return;
+        }
 
-      // Skip update if name is unchanged or empty.
-      if (!newRoomName || oldRoomName === newRoomName) {
-        return;
-      }
+        var roomData = {};
+        var context = room.decryptedContext;
+        var oldRoomName = context.roomName;
+        var newRoomName = actionData.newRoomName.trim();
+        if (newRoomName && oldRoomName != newRoomName) {
+          roomData.roomName = newRoomName;
+        }
+        var oldRoomURLs = context.urls;
+        var oldRoomURL = oldRoomURLs && oldRoomURLs[0];
+        // Since we want to prevent storing falsy (i.e. empty) values for context
+        // data, there's no need to send that to the server as an update.
+        var newRoomURL = loop.shared.utils.stripFalsyValues({
+          location: actionData.newRoomURL ? actionData.newRoomURL.trim() : "",
+          thumbnail: actionData.newRoomURL ? actionData.newRoomThumbnail.trim() : "",
+          description: actionData.newRoomDescription ?
+            actionData.newRoomDescription.trim() : ""
+        });
+        // Only attach a context to the room when
+        // 1) there was already a URL set,
+        // 2) a new URL is provided as of now,
+        // 3) the URL data has changed.
+        var diff = loop.shared.utils.objectDiff(oldRoomURL, newRoomURL);
+        if (diff.added.length || diff.updated.length) {
+          newRoomURL = _.extend(oldRoomURL || {}, newRoomURL);
+          var isValidURL = false;
+          try {
+            isValidURL = new URL(newRoomURL.location);
+          } catch(ex) {}
+          if (isValidURL) {
+            roomData.urls = [newRoomURL];
+          }
+        }
+        // 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.
 
-      this.setStoreState({error: null});
-      this._mozLoop.rooms.rename(actionData.roomToken, newRoomName,
-        function(err) {
-          if (err) {
-            this.dispatchAction(new sharedActions.RenameRoomError({error: err}));
-          }
-        }.bind(this));
+        // When no properties have been set on the roomData object, there's nothing
+        // to save.
+        if (!Object.getOwnPropertyNames(roomData).length) {
+          return;
+        }
+
+        this.setStoreState({error: null});
+        this._mozLoop.rooms.update(actionData.roomToken, roomData,
+          function(err, data) {
+            if (err) {
+              this.dispatchAction(new sharedActions.UpdateRoomContextError({
+                error: err
+              }));
+            }
+          }.bind(this));
+      }.bind(this));
     },
 
-    renameRoomError: function(actionData) {
+    /**
+     * Updating the context data attached to a room error.
+     *
+     * @param {sharedActions.UpdateRoomContextError} actionData
+     */
+    updateRoomContextError: function(actionData) {
       this.setStoreState({error: actionData.error});
     }
   });
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -165,53 +165,35 @@ loop.roomViews = (function(mozL10n) {
       );
     }
   });
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({displayName: "DesktopRoomInvitationView",
-    mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
+    mixins: [sharedMixins.DropdownMenuMixin],
 
     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,
       show: React.PropTypes.bool.isRequired,
       showContext: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
-        newRoomName: ""
+        editMode: false
       };
     },
 
-    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.props.roomData.roomToken,
-        newRoomName: this.state.newRoomName
-      }));
-    },
-
     handleEmailButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
     },
 
     handleCopyButtonClick: function(event) {
@@ -224,37 +206,45 @@ loop.roomViews = (function(mozL10n) {
     },
 
     handleShareButtonClick: function(event) {
       event.preventDefault();
 
       this.toggleDropdownMenu();
     },
 
+    handleAddContextClick: function(event) {
+      event.preventDefault();
+
+      this.handleEditModeChange(true);
+    },
+
+    handleEditModeChange: function(newEditMode) {
+      this.setState({ editMode: newEditMode });
+    },
+
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
+      var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
+        !this.props.showContext && !this.state.editMode;
+
       var cx = React.addons.classSet;
       return (
         React.createElement("div", {className: "room-invitation-overlay"}, 
           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("p", {className: cx({hide: this.state.editMode})}, 
+              mozL10n.get("invite_header_text")
             ), 
-            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("a", {className: cx({hide: !canAddContext, "room-invitation-addcontext": true}), 
+               onClick: this.handleAddContextClick}, 
+              mozL10n.get("context_add_some_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") :
@@ -270,78 +260,291 @@ loop.roomViews = (function(mozL10n) {
               dispatcher: this.props.dispatcher, 
               roomUrl: this.props.roomData.roomUrl, 
               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, 
+            mozLoop: this.props.mozLoop, 
+            onEditModeChange: this.handleEditModeChange, 
             roomData: this.props.roomData, 
-            show: this.props.showContext})
+            show: this.props.showContext || this.state.editMode})
         )
       );
     }
   });
 
   var DesktopRoomContextView = React.createClass({displayName: "DesktopRoomContextView",
+    mixins: [React.addons.LinkedStateMixin],
+
     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,
       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) {
-        this.setState({ show: nextProps.show });
+        newState.show = nextProps.show;
+      }
+      if (("editMode" in nextProps && nextProps.editMode !== this.props.editMode)) {
+        newState.editMode = nextProps.editMode;
+        // If we're switching to edit mode, fetch the metadata of the current tab.
+        // But _only_ if there's no context currently attached to the room; the
+        // checkbox will be disabled in that case.
+        if (nextProps.editMode) {
+          this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
+            var previewImage = metadata.previews.length ? metadata.previews[0] : "";
+            var description = metadata.description || metadata.title;
+            var url = metadata.url;
+            this.setState({
+              availableContext: {
+                previewImage: previewImage,
+                description: description,
+                url: url
+              }
+           });
+          }.bind(this));
+        }
+      }
+      // When we receive an update for the `roomData` property, make sure that
+      // the current form fields reflect reality. This is necessary, because the
+      // form state is maintained in the components' state.
+      if (nextProps.roomData) {
+        // Right now it's only necessary to update the form input states when
+        // they contain no text yet.
+        if (!this.state.newRoomName && nextProps.roomData.roomName) {
+          newState.newRoomName = nextProps.roomData.roomName;
+        }
+        var url = this._getURL(nextProps.roomData);
+        if (url) {
+          if (!this.state.newRoomURL && url.location) {
+            newState.newRoomURL = url.location;
+          }
+          if (!this.state.newRoomDescription && url.description) {
+            newState.newRoomDescription = url.description;
+          }
+          if (!this.state.newRoomThumbnail && url.thumbnail) {
+            newState.newRoomThumbnail = url.thumbnail;
+          }
+        }
+      }
+
+      if (Object.getOwnPropertyNames(newState).length) {
+        this.setState(newState);
+      }
+    },
+
+    getDefaultProps: function() {
+      return { editMode: false };
+     },
+
+    getInitialState: function() {
+      var url = this._getURL();
+      return {
+        // `availableContext` prop only used in tests.
+        availableContext: this.props.availableContext,
+        editMode: this.props.editMode,
+        show: this.props.show,
+        newRoomName: this.props.roomData.roomName || "",
+        newRoomURL: url && url.location || "",
+        newRoomDescription: url && url.description || "",
+        newRoomThumbnail: url && url.thumbnail || ""
+      };
+    },
+
+    handleCloseClick: function(event) {
+      event.preventDefault();
+
+      if (this.state.editMode) {
+        this.setState({ editMode: false });
+        if (this.props.onEditModeChange) {
+          this.props.onEditModeChange(false);
+        }
+        return;
+      }
+      this.setState({ show: false });
+    },
+
+    handleEditClick: function(event) {
+      event.preventDefault();
+
+      this.setState({ editMode: true });
+      if (this.props.onEditModeChange) {
+        this.props.onEditModeChange(true);
       }
     },
 
-    getInitialState: function() {
-      return { show: this.props.show };
+    handleCheckboxChange: function(state) {
+      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,
+        newRoomName: this.state.newRoomName,
+        newRoomURL: this.state.newRoomURL,
+        newRoomDescription: this.state.newRoomDescription,
+        newRoomThumbnail: this.state.newRoomThumbnail
+      }));
     },
 
-    handleCloseClick: function() {
-      this.setState({ show: false });
+    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);
+      }
+    },
+
+    /**
+     * Utility function to extract URL context data from the `roomData` property
+     * that can also be supplied as an argument.
+     *
+     * @param  {Object} roomData Optional room data object to use, equivalent to
+     *                           the activeRoomStore state.
+     * @return {Object} The first context URL found on the `roomData` object.
+     */
+    _getURL: function(roomData) {
+      roomData = roomData || this.props.roomData;
+      return this.props.roomData.roomContextUrls &&
+        this.props.roomData.roomContextUrls[0];
+    },
+
+    /**
+     * Truncate a string if it exceeds the length as defined in `maxLen`, which
+     * is defined as '72' characters by default. If the string needs trimming,
+     * it'll be suffixed with the unicode ellipsis char, \u2026.
+     *
+     * @param  {String} str    The string to truncate, if needed.
+     * @param  {Number} maxLen Maximum number of characters that the string is
+     *                         allowed to contain. Optional, defaults to 72.
+     * @return {String} Truncated version of `str`.
+     */
+    _truncate: function(str, maxLen) {
+      if (!maxLen) {
+        maxLen = 72;
+      }
+      return (str.length > maxLen) ? str.substr(0, maxLen) + "…" : str;
     },
 
     render: function() {
-      if (!this.state.show)
+      if (!this.state.show && !this.state.editMode)
         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 || "";
+      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 = sharedUtils.formatURL(location);
+        locationData = checkboxLabel = sharedUtils.formatURL(location);
+      }
+      if (!checkboxLabel) {
+        checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
+          this.state.availableContext.url : ""));
+      }
+
+      var cx = React.addons.classSet;
+      if (this.state.editMode) {
+        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})
+            )
+          )
+        );
       }
 
       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-description"}, URLDescription), 
+            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})
+                    onClick: this.handleCloseClick}), 
+            React.createElement("button", {className: "room-context-btn-edit", 
+                    onClick: this.handleEditClick})
           )
         )
       );
     }
   });
 
   /**
    * Desktop room conversation view.
@@ -452,16 +655,17 @@ loop.roomViews = (function(mozL10n) {
           );
         }
         default: {
           return (
             React.createElement("div", {className: "room-conversation-wrapper"}, 
               React.createElement(DesktopRoomInvitationView, {
                 dispatcher: this.props.dispatcher, 
                 error: this.state.error, 
+                mozLoop: this.props.mozLoop, 
                 roomData: roomData, 
                 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"}, 
@@ -476,16 +680,19 @@ loop.roomViews = (function(mozL10n) {
                     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, {
+                dispatcher: this.props.dispatcher, 
+                error: this.state.error, 
+                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
@@ -165,53 +165,35 @@ loop.roomViews = (function(mozL10n) {
       );
     }
   });
 
   /**
    * Desktop room invitation view (overlay).
    */
   var DesktopRoomInvitationView = React.createClass({
-    mixins: [React.addons.LinkedStateMixin, sharedMixins.DropdownMenuMixin],
+    mixins: [sharedMixins.DropdownMenuMixin],
 
     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,
       show: React.PropTypes.bool.isRequired,
       showContext: React.PropTypes.bool.isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
-        newRoomName: ""
+        editMode: false
       };
     },
 
-    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.props.roomData.roomToken,
-        newRoomName: this.state.newRoomName
-      }));
-    },
-
     handleEmailButtonClick: function(event) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.EmailRoomUrl({roomUrl: this.props.roomData.roomUrl}));
     },
 
     handleCopyButtonClick: function(event) {
@@ -224,37 +206,45 @@ loop.roomViews = (function(mozL10n) {
     },
 
     handleShareButtonClick: function(event) {
       event.preventDefault();
 
       this.toggleDropdownMenu();
     },
 
+    handleAddContextClick: function(event) {
+      event.preventDefault();
+
+      this.handleEditModeChange(true);
+    },
+
+    handleEditModeChange: function(newEditMode) {
+      this.setState({ editMode: newEditMode });
+    },
+
     render: function() {
       if (!this.props.show) {
         return null;
       }
 
+      var canAddContext = this.props.mozLoop.getLoopPref("contextInConversations.enabled") &&
+        !this.props.showContext && !this.state.editMode;
+
       var cx = React.addons.classSet;
       return (
         <div className="room-invitation-overlay">
           <div className="room-invitation-content">
-            <p className={cx({"error": !!this.props.error,
-                              "error-display-area": true})}>
-              {mozL10n.get("rooms_name_change_failed_label")}
+            <p className={cx({hide: this.state.editMode})}>
+              {mozL10n.get("invite_header_text")}
             </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>
+            <a className={cx({hide: !canAddContext, "room-invitation-addcontext": true})}
+               onClick={this.handleAddContextClick}>
+              {mozL10n.get("context_add_some_label")}
+            </a>
             <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") :
@@ -270,78 +260,291 @@ loop.roomViews = (function(mozL10n) {
               dispatcher={this.props.dispatcher}
               roomUrl={this.props.roomData.roomUrl}
               show={this.state.showMenu}
               socialShareButtonAvailable={this.props.socialShareButtonAvailable}
               socialShareProviders={this.props.socialShareProviders}
               ref="menu" />
           </div>
           <DesktopRoomContextView
+            dispatcher={this.props.dispatcher}
+            editMode={this.state.editMode}
+            error={this.props.error}
+            mozLoop={this.props.mozLoop}
+            onEditModeChange={this.handleEditModeChange}
             roomData={this.props.roomData}
-            show={this.props.showContext} />
+            show={this.props.showContext || this.state.editMode} />
         </div>
       );
     }
   });
 
   var DesktopRoomContextView = React.createClass({
+    mixins: [React.addons.LinkedStateMixin],
+
     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,
       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) {
-        this.setState({ show: nextProps.show });
+        newState.show = nextProps.show;
+      }
+      if (("editMode" in nextProps && nextProps.editMode !== this.props.editMode)) {
+        newState.editMode = nextProps.editMode;
+        // If we're switching to edit mode, fetch the metadata of the current tab.
+        // But _only_ if there's no context currently attached to the room; the
+        // checkbox will be disabled in that case.
+        if (nextProps.editMode) {
+          this.props.mozLoop.getSelectedTabMetadata(function(metadata) {
+            var previewImage = metadata.previews.length ? metadata.previews[0] : "";
+            var description = metadata.description || metadata.title;
+            var url = metadata.url;
+            this.setState({
+              availableContext: {
+                previewImage: previewImage,
+                description: description,
+                url: url
+              }
+           });
+          }.bind(this));
+        }
+      }
+      // When we receive an update for the `roomData` property, make sure that
+      // the current form fields reflect reality. This is necessary, because the
+      // form state is maintained in the components' state.
+      if (nextProps.roomData) {
+        // Right now it's only necessary to update the form input states when
+        // they contain no text yet.
+        if (!this.state.newRoomName && nextProps.roomData.roomName) {
+          newState.newRoomName = nextProps.roomData.roomName;
+        }
+        var url = this._getURL(nextProps.roomData);
+        if (url) {
+          if (!this.state.newRoomURL && url.location) {
+            newState.newRoomURL = url.location;
+          }
+          if (!this.state.newRoomDescription && url.description) {
+            newState.newRoomDescription = url.description;
+          }
+          if (!this.state.newRoomThumbnail && url.thumbnail) {
+            newState.newRoomThumbnail = url.thumbnail;
+          }
+        }
+      }
+
+      if (Object.getOwnPropertyNames(newState).length) {
+        this.setState(newState);
+      }
+    },
+
+    getDefaultProps: function() {
+      return { editMode: false };
+     },
+
+    getInitialState: function() {
+      var url = this._getURL();
+      return {
+        // `availableContext` prop only used in tests.
+        availableContext: this.props.availableContext,
+        editMode: this.props.editMode,
+        show: this.props.show,
+        newRoomName: this.props.roomData.roomName || "",
+        newRoomURL: url && url.location || "",
+        newRoomDescription: url && url.description || "",
+        newRoomThumbnail: url && url.thumbnail || ""
+      };
+    },
+
+    handleCloseClick: function(event) {
+      event.preventDefault();
+
+      if (this.state.editMode) {
+        this.setState({ editMode: false });
+        if (this.props.onEditModeChange) {
+          this.props.onEditModeChange(false);
+        }
+        return;
+      }
+      this.setState({ show: false });
+    },
+
+    handleEditClick: function(event) {
+      event.preventDefault();
+
+      this.setState({ editMode: true });
+      if (this.props.onEditModeChange) {
+        this.props.onEditModeChange(true);
       }
     },
 
-    getInitialState: function() {
-      return { show: this.props.show };
+    handleCheckboxChange: function(state) {
+      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,
+        newRoomName: this.state.newRoomName,
+        newRoomURL: this.state.newRoomURL,
+        newRoomDescription: this.state.newRoomDescription,
+        newRoomThumbnail: this.state.newRoomThumbnail
+      }));
     },
 
-    handleCloseClick: function() {
-      this.setState({ show: false });
+    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);
+      }
+    },
+
+    /**
+     * Utility function to extract URL context data from the `roomData` property
+     * that can also be supplied as an argument.
+     *
+     * @param  {Object} roomData Optional room data object to use, equivalent to
+     *                           the activeRoomStore state.
+     * @return {Object} The first context URL found on the `roomData` object.
+     */
+    _getURL: function(roomData) {
+      roomData = roomData || this.props.roomData;
+      return this.props.roomData.roomContextUrls &&
+        this.props.roomData.roomContextUrls[0];
+    },
+
+    /**
+     * Truncate a string if it exceeds the length as defined in `maxLen`, which
+     * is defined as '72' characters by default. If the string needs trimming,
+     * it'll be suffixed with the unicode ellipsis char, \u2026.
+     *
+     * @param  {String} str    The string to truncate, if needed.
+     * @param  {Number} maxLen Maximum number of characters that the string is
+     *                         allowed to contain. Optional, defaults to 72.
+     * @return {String} Truncated version of `str`.
+     */
+    _truncate: function(str, maxLen) {
+      if (!maxLen) {
+        maxLen = 72;
+      }
+      return (str.length > maxLen) ? str.substr(0, maxLen) + "…" : str;
     },
 
     render: function() {
-      if (!this.state.show)
+      if (!this.state.show && !this.state.editMode)
         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 || "";
+      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 = sharedUtils.formatURL(location);
+        locationData = checkboxLabel = sharedUtils.formatURL(location);
+      }
+      if (!checkboxLabel) {
+        checkboxLabel = sharedUtils.formatURL((this.state.availableContext ?
+          this.state.availableContext.url : ""));
+      }
+
+      var cx = React.addons.classSet;
+      if (this.state.editMode) {
+        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}/>
+            </div>
+          </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-description">{URLDescription}</div>
+            <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}/>
+            <button className="room-context-btn-edit"
+                    onClick={this.handleEditClick}/>
           </div>
         </div>
       );
     }
   });
 
   /**
    * Desktop room conversation view.
@@ -452,16 +655,17 @@ loop.roomViews = (function(mozL10n) {
           );
         }
         default: {
           return (
             <div className="room-conversation-wrapper">
               <DesktopRoomInvitationView
                 dispatcher={this.props.dispatcher}
                 error={this.state.error}
+                mozLoop={this.props.mozLoop}
                 roomData={roomData}
                 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">
@@ -476,16 +680,19 @@ loop.roomViews = (function(mozL10n) {
                     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
+                dispatcher={this.props.dispatcher}
+                error={this.state.error}
+                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
@@ -869,53 +869,48 @@ html, .fx-embedded, #main,
   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;
 }
 
-.room-invitation-overlay .error-display-area {
-  display: none;
-}
-
-.room-invitation-overlay .error-display-area.error {
-  position: absolute;
-  top: 2em;
-  left: 1em;
-  right: 1em;
-  text-align: start;
-  width: calc(258px - 2em);
-  color: #d74345;
-}
-
 .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-addcontext {
+  color: #0095dd;
+  padding-left: 1.5em;
+  margin-bottom: 1em;
+  background-image: url("../img/icons-10x10.svg#edit-active");
+  background-size: 1em 1em;
+  background-repeat: no-repeat;
+  background-position: left top;
+  font-size: 1em;
+  cursor: pointer;
+}
+
+.room-invitation-addcontext:hover,
+.room-invitation-addcontext:hover:active {
+  text-decoration: underline;
+}
+
+body[dir="rtl"] .room-invitation-addcontext {
+  padding-left: auto;
+  padding-right: 1.5em;
+  background-position: right top;
 }
 
 .share-service-dropdown {
   color: #000;
   text-align: start;
   bottom: auto;
   top: 0;
 }
@@ -970,39 +965,40 @@ body[dir=rtl] .share-service-dropdown .s
   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;
+  max-height: 400px;
   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;
+  overflow-x: hidden;
+  overflow-y: auto;
 }
 
 .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;
@@ -1017,22 +1013,44 @@ body[dir=rtl] .room-context-thumbnail {
   flex: 1 1 auto;
   text-align: start;
 }
 
 body[dir=rtl] .room-context-content {
   order: 1;
 }
 
+.room-context-content > .error-display-area.error {
+  display: block;
+  background-color: rgba(215,67,69,.8);
+  border-radius: 3px;
+  padding: .5em;
+}
+
+.room-context-content > .error-display-area {
+  display: none;
+}
+
+.room-context-content > .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 {
+  margin-bottom: .5em;
+}
+
 .room-context-label {
   margin-bottom: 1em;
 }
 
 .room-context-label,
-.room-context-description {
+.room-context-description,
+.room-context-content > .checkbox-wrapper > label {
   color: #fff;
 }
 
 .room-context-comment {
   color: #707070;
 }
 
 .room-context-description,
@@ -1046,41 +1064,76 @@ body[dir=rtl] .room-context-content {
   text-decoration: none;
   margin-bottom: 1em;
 }
 
 .room-context-url:hover {
   text-decoration: underline;
 }
 
-.room-context-btn-close {
+.room-context-content > form > textarea,
+.room-context-content > 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;
+  width: 100%;
+  padding: .2em .4em;
+  border-radius: 3px;
+  resize: none;
+}
+
+.room-context-content > form > textarea:not(:last-of-type),
+.room-context-content > form > input[type="text"] {
+  margin: 0 0 .5em 0;
+}
+
+.room-context-btn-close,
+.room-context-btn-edit {
   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-edit {
+  right: 18px;
+  background-image: url("../img/icons-10x10.svg#edit");
+}
+
+.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-close,
+body[dir=rtl] .room-context-btn-edit {
   right: auto;
   left: 5px;
 }
 
+body[dir=rtl] .room-context-btn-edit {
+  left: 18px;
+}
+
 /* 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
@@ -34,24 +34,32 @@ use[id$="-disabled"] {
 }
 </style>
 <defs style="display:none">
   <polygon id="close-shape" fill-rule="evenodd" clip-rule="evenodd" 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" clip-rule="evenodd" d="M9,3L4.984,7L1,3H9z"/>
   <polygon id="expand-shape" fill-rule="evenodd" clip-rule="evenodd" 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" fill-rule="evenodd" clip-rule="evenodd" 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="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="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
@@ -325,29 +325,31 @@ loop.shared.actions = (function() {
      * Opens a room.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     OpenRoom: Action.define("openRoom", {
       roomToken: String
     }),
 
     /**
-     * Renames a room.
+     * Updates the context data attached to a room.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
-    RenameRoom: Action.define("renameRoom", {
+    UpdateRoomContext: Action.define("updateRoomContext", {
       roomToken: String,
       newRoomName: String
+      // newRoomDescription: String, Optional.
+      // newRoomThumbnail: String, Optional.
+      // newRoomURL: String Optional.
     }),
 
     /**
-     * Renaming a room error.
-     * XXX: should move to some roomActions module - refs bug 1079284
+     * Updating the context data attached to a room error.
      */
-    RenameRoomError: Action.define("renameRoomError", {
+    UpdateRoomContextError: Action.define("updateRoomContextError", {
       error: [Error, Object]
     }),
 
     /**
      * Copy a room url into the user's clipboard.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     CopyRoomUrl: Action.define("copyRoomUrl", {
--- a/browser/components/loop/modules/LoopRooms.jsm
+++ b/browser/components/loop/modules/LoopRooms.jsm
@@ -705,50 +705,57 @@ let LoopRoomsInternal = {
       connections: status.connections,
       sendStreams: status.sendStreams,
       recvStreams: status.recvStreams,
       sessionToken: sessionToken
     }, callback);
   },
 
   /**
-   * Renames a room.
+   * Updates a room.
    *
-   * @param {String} roomToken   The room token
-   * @param {String} newRoomName The new name for the room
-   * @param {Function} callback   Function that will be invoked once the operation
-   *                              finished. The first argument passed will be an
-   *                              `Error` object or `null`.
+   * @param {String} roomToken  The room token
+   * @param {Object} roomData   Updated context data for the room. The following
+   *                            properties are expected: `roomName` and `urls`.
+   *                            IMPORTANT: Data in the `roomData::urls` array
+   *                            will be stored as-is, so any data omitted therein
+   *                            will be gone forever.
+   * @param {Function} callback Function that will be invoked once the operation
+   *                            finished. The first argument passed will be an
+   *                            `Error` object or `null`.
    */
-  rename: function(roomToken, newRoomName, callback) {
+  update: function(roomToken, roomData, callback) {
     let room = this.rooms.get(roomToken);
     let url = "/rooms/" + encodeURIComponent(roomToken);
 
-    let roomData = this.rooms.get(roomToken);
-    if (!roomData.decryptedContext) {
-      roomData.decryptedContext = {
-        roomName: newRoomName
+    if (!room.decryptedContext) {
+      room.decryptedContext = {
+        roomName: roomData.roomName || room.roomName
       };
     } else {
-      roomData.decryptedContext.roomName = newRoomName;
+      room.decryptedContext.roomName = roomData.roomName || room.roomName;
+    }
+    if (roomData.urls && roomData.urls.length) {
+      // For now we only support adding one URL to the room context.
+      room.decryptedContext.urls = [roomData.urls[0]];
     }
 
     Task.spawn(function* () {
-      let {all, encrypted} = yield this.promiseEncryptRoomData(roomData);
+      let {all, encrypted} = yield this.promiseEncryptRoomData(room);
 
       // For patch, we only send the context data.
       let sendData = {
         context: encrypted.context
       };
 
       // If we're not encrypting currently, then only send the roomName.
       // XXX This should go away once bug 1153788 is fixed.
       if (!sendData.context) {
         sendData = {
-          roomName: newRoomName
+          roomName: room.decryptedContext.roomName
         };
       } else {
         // This might be an upgrade to encrypted rename, so store the key
         // just in case.
         yield this.roomsCache.setKey(this.sessionType, all.roomToken, all.roomKey);
       }
 
       let response = yield MozLoopService.hawkRequest(this.sessionType,
@@ -859,18 +866,18 @@ this.LoopRooms = {
   leave: function(roomToken, sessionToken, callback) {
     return LoopRoomsInternal.leave(roomToken, sessionToken, callback);
   },
 
   sendConnectionStatus: function(roomToken, sessionToken, status, callback) {
     return LoopRoomsInternal.sendConnectionStatus(roomToken, sessionToken, status, callback);
   },
 
-  rename: function(roomToken, newRoomName, callback) {
-    return LoopRoomsInternal.rename(roomToken, newRoomName, callback);
+  update: function(roomToken, roomData, callback) {
+    return LoopRoomsInternal.update(roomToken, roomData, callback);
   },
 
   getGuestCreatedRoom: function() {
     return LoopRoomsInternal.getGuestCreatedRoom();
   },
 
   maybeRefresh: function(user) {
     return LoopRoomsInternal.maybeRefresh(user);
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -110,17 +110,16 @@ first_time_experience_button_label=Get S
 help_label=Help
 tour_label=Tour
 
 rooms_default_room_name_template=Conversation {{conversationLabel}}
 rooms_leave_button_label=Leave
 rooms_list_copy_url_tooltip=Copy Link
 rooms_list_delete_tooltip=Delete conversation
 rooms_list_deleteConfirmation_label=Are you sure?
-rooms_name_this_room_label=Name this conversation
 rooms_new_room_button_label=Start a conversation
 rooms_only_occupant_label=You're the first one here.
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -159,19 +159,19 @@
         }})
       );
     }
   });
 
   var SVGIcons = React.createClass({displayName: "SVGIcons",
     shapes: {
       "10x10": ["close", "close-active", "close-disabled", "dropdown",
-        "dropdown-white", "dropdown-active", "dropdown-disabled", "expand",
-        "expand-active", "expand-disabled", "minimize", "minimize-active",
-        "minimize-disabled"
+        "dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
+        "edit-active", "edit-disabled", "expand", "expand-active", "expand-disabled",
+        "minimize", "minimize-active", "minimize-disabled"
       ],
       "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
         "facemute-active", "facemute-disabled", "hangup", "hangup-active",
         "hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
         "link", "link-active", "link-disabled", "mute", "mute-active",
         "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
         "video-white", "video-active", "video-disabled", "volume", "volume-active",
         "volume-disabled"
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -159,19 +159,19 @@
         }} />
       );
     }
   });
 
   var SVGIcons = React.createClass({
     shapes: {
       "10x10": ["close", "close-active", "close-disabled", "dropdown",
-        "dropdown-white", "dropdown-active", "dropdown-disabled", "expand",
-        "expand-active", "expand-disabled", "minimize", "minimize-active",
-        "minimize-disabled"
+        "dropdown-white", "dropdown-active", "dropdown-disabled", "edit",
+        "edit-active", "edit-disabled", "expand", "expand-active", "expand-disabled",
+        "minimize", "minimize-active", "minimize-disabled"
       ],
       "14x14": ["audio", "audio-active", "audio-disabled", "facemute",
         "facemute-active", "facemute-disabled", "hangup", "hangup-active",
         "hangup-disabled", "incoming", "incoming-active", "incoming-disabled",
         "link", "link-active", "link-disabled", "mute", "mute-active",
         "mute-disabled", "pause", "pause-active", "pause-disabled", "video",
         "video-white", "video-active", "video-disabled", "volume", "volume-active",
         "volume-disabled"
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -301,18 +301,17 @@ rooms_list_copy_url_tooltip=Copy Link
 ## Semicolon-separated list of plural forms. See:
 ## http://developer.mozilla.org/en/docs/Localization_and_Plurals
 ## We prefer to have no number in the string, but if you need it for your
 ## language please use {{num}}.
 rooms_list_current_conversations=Current conversation;Current conversations
 rooms_list_delete_tooltip=Delete conversation
 rooms_list_deleteConfirmation_label=Are you sure?
 rooms_list_no_current_conversations=No current conversations
-rooms_name_this_room_label=Name this conversation
-rooms_name_change_failed_label=Conversation cannot be renamed
+rooms_change_failed_label=Conversation cannot be updated
 rooms_new_room_button_label=Start a conversation
 rooms_only_occupant_label=You're the first one here.
 rooms_panel_title=Choose a conversation or start a new one
 rooms_room_full_label=There are already two people in this conversation.
 rooms_room_full_call_to_action_nonFx_label=Download {{brandShortname}} to start your own
 rooms_room_full_call_to_action_label=Learn more about {{clientShortname}} »
 rooms_room_joined_label=Someone has joined the conversation!
 rooms_room_join_label=Join the conversation
@@ -335,8 +334,9 @@ context_offer_label=Let's talk about thi
 # https://bug1115342.bugzilla.mozilla.org/attachment.cgi?id=8563677
 context_inroom_label=Let's talk about:
 ## LOCALIZATION_NOTE (context_edit_activate_label): {{title}} will be replaced
 ## by the title of the active tab, also known as the title of an HTML document.
 ## The quotes around the title are intentional.
 context_edit_activate_label=Talk about "{{title}}"
 context_edit_name_placeholder=Conversation Name
 context_edit_comments_placeholder=Comments
+context_add_some_label=Add some context