Bug 1100595 - Add UI for indicating if renaming a room failed. r=NiKo` a=lsblakk
authorJared Wein <jwein@mozilla.com>
Tue, 16 Dec 2014 13:23:16 -0500
changeset 242555 4c6a7ec58300ebeaa4cfce7c4d5ef0479690b093
parent 242554 0e7c88cc3a61b0456b7823f87f925efe95026b1f
child 242556 b58c6f395602fdd30e8c55818272828efbab9e2f
push id4311
push userraliiev@mozilla.com
push dateMon, 12 Jan 2015 19:37:41 +0000
treeherdermozilla-beta@150c9fed433b [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersNiKo, lsblakk
bugs1100595
milestone36.0a2
Bug 1100595 - Add UI for indicating if renaming a room failed. r=NiKo` a=lsblakk
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/roomStore.js
browser/components/loop/test/shared/roomStore_test.js
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -62,20 +62,30 @@ loop.roomViews = (function(mozL10n) {
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
-        newRoomName: ""
+        newRoomName: "",
+        error: null,
       };
     },
 
+    componentWillMount: function() {
+      this.listenTo(this.props.roomStore, "change:error",
+                    this.onRoomError);
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.roomStore);
+    },
+
     handleFormSubmit: function(event) {
       event.preventDefault();
 
       var newRoomName = this.state.newRoomName;
 
       if (newRoomName && this.state.roomName != newRoomName) {
         this.props.dispatcher.dispatch(
           new sharedActions.RenameRoom({
@@ -96,19 +106,33 @@ loop.roomViews = (function(mozL10n) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
 
       this.setState({copiedUrl: true});
     },
 
+    onRoomError: function() {
+      // Only update the state if we're mounted, to avoid the problem where
+      // stopListening doesn't nuke the active listeners during a event
+      // processing.
+      if (this.isMounted()) {
+        this.setState({error: this.props.roomStore.getStoreState("error")});
+      }
+    },
+
     render: function() {
+      var cx = React.addons.classSet;
       return (
         React.DOM.div({className: "room-invitation-overlay"}, 
+          React.DOM.p({className: cx({"error": !!this.state.error,
+                            "error-display-area": true})}, 
+            mozL10n.get("rooms_name_change_failed_label")
+          ), 
           React.DOM.form({onSubmit: this.handleFormSubmit}, 
             React.DOM.input({type: "text", className: "input-room-name", 
               valueLink: this.linkState("newRoomName"), 
               onBlur: this.handleFormSubmit, 
               placeholder: mozL10n.get("rooms_name_this_room_label")})
           ), 
           React.DOM.p(null, mozL10n.get("invite_header_text")), 
           React.DOM.div({className: "btn-group call-action-group"}, 
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -62,20 +62,30 @@ loop.roomViews = (function(mozL10n) {
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
     },
 
     getInitialState: function() {
       return {
         copiedUrl: false,
-        newRoomName: ""
+        newRoomName: "",
+        error: null,
       };
     },
 
+    componentWillMount: function() {
+      this.listenTo(this.props.roomStore, "change:error",
+                    this.onRoomError);
+    },
+
+    componentWillUnmount: function() {
+      this.stopListening(this.props.roomStore);
+    },
+
     handleFormSubmit: function(event) {
       event.preventDefault();
 
       var newRoomName = this.state.newRoomName;
 
       if (newRoomName && this.state.roomName != newRoomName) {
         this.props.dispatcher.dispatch(
           new sharedActions.RenameRoom({
@@ -96,19 +106,33 @@ loop.roomViews = (function(mozL10n) {
       event.preventDefault();
 
       this.props.dispatcher.dispatch(
         new sharedActions.CopyRoomUrl({roomUrl: this.state.roomUrl}));
 
       this.setState({copiedUrl: true});
     },
 
+    onRoomError: function() {
+      // Only update the state if we're mounted, to avoid the problem where
+      // stopListening doesn't nuke the active listeners during a event
+      // processing.
+      if (this.isMounted()) {
+        this.setState({error: this.props.roomStore.getStoreState("error")});
+      }
+    },
+
     render: function() {
+      var cx = React.addons.classSet;
       return (
         <div className="room-invitation-overlay">
+          <p className={cx({"error": !!this.state.error,
+                            "error-display-area": true})}>
+            {mozL10n.get("rooms_name_change_failed_label")}
+          </p>
           <form onSubmit={this.handleFormSubmit}>
             <input type="text" className="input-room-name"
               valueLink={this.linkState("newRoomName")}
               onBlur={this.handleFormSubmit}
               placeholder={mozL10n.get("rooms_name_this_room_label")} />
           </form>
           <p>{mozL10n.get("invite_header_text")}</p>
           <div className="btn-group call-action-group">
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -152,17 +152,17 @@
 }
 
 /* Common media control buttons behavior */
 .conversation-toolbar .media-control {
   background-color: transparent;
   opacity: 1;
 }
 .conversation-toolbar .media-control:hover {
-  background-color: rgba(255, 255, 255, .35);
+  background-color: rgba(255,255,255,.35);
   opacity: 1;
 }
 .conversation-toolbar .media-control.muted {
   background-color: #0096DD;
   opacity: 1;
 }
 
 /* Audio mute button */
@@ -203,17 +203,17 @@
   right: 0px;
   bottom: 0px;
   left: 0px;
 }
 
 .standalone .local-stream {
   /* required to have it superimposed to the control toolbar */
   z-index: 1001;
-  box-shadow: 0 2px 4px rgba(0, 0, 0, 0.5);
+  box-shadow: 0 2px 4px rgba(0,0,0,.5);
 }
 
 /* Side by side video elements */
 
 .conversation .media.side-by-side .remote {
   width: 50%;
   float: left;
 }
@@ -315,17 +315,17 @@
 
 .native-dropdown-menu,
 .native-dropdown-large-parent {
   /* Should match a native select menu */
   padding: 0;
   position: absolute; /* element can be wider than the parent */
   background: #fff;
   margin: 0;
-  box-shadow: 0 4px 5px rgba(30, 30, 30, .3);
+  box-shadow: 0 4px 5px rgba(30,30,30,.3);
   border-style: solid;
   border-width: 1px 1px 1px 2px;
   border-color: #aaa #111 #111 #aaa;
 }
 
   /*
    * If the component is smaller than the parent
    * we need it to display block to occupy full width
@@ -468,17 +468,17 @@
   position: absolute;
   right: 3px;
   bottom: 5px;
   /* next two lines are workaround for lack of object-fit; see bug 1020445 */
   max-width: 140px;
   width: 30%;
   height: 28%;
   max-height: 105px;
-  box-shadow: 0px 2px 4px rgba(0, 0, 0, 0.5);
+  box-shadow: 0px 2px 4px rgba(0,0,0,.5);
 }
 
 .fx-embedded .local-stream.room-preview {
   top: 0px;
   left: 0px;
   right: 0px;
   bottom: 0px;
   height: 100%;
@@ -561,17 +561,17 @@
   min-width: 50px;
   background: #ccc;
   border-radius: 50%;
   background-image: url("../img/audio-call-avatar.svg");
   background-repeat: no-repeat;
   background-color: #4ba6e7;
   background-size: contain;
   overflow: hidden;
-  box-shadow: inset 0 0 0 1px rgba(255, 255, 255, 0.3);
+  box-shadow: inset 0 0 0 1px rgba(255,255,255,.3);
   float: left;
   -moz-margin-end: 1em;
 }
 
 .fx-embedded-call-identifier-text {
   font-weight: bold;
 }
 
@@ -771,39 +771,57 @@ html, .fx-embedded, #main,
 }
 
 .fx-embedded .room-conversation .conversation-toolbar .btn-hangup {
   background-image: url("../img/icons-16x16.svg#leave");
 }
 
 .room-invitation-overlay {
   position: absolute;
-  background: rgba(0, 0, 0, .6);
+  background: rgba(0,0,0,.6);
   /* This matches .fx-embedded .conversation toolbar height */
   top: 26px;
   right: 0;
   bottom: 0;
   left: 0;
   text-align: center;
   color: #fff;
   z-index: 1010;
 }
 
+.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 form {
   padding: 8em 0 2.5em 0;
 }
 
 .room-invitation-overlay input[type="text"] {
-  display: block;
-  background: rgba(0, 0, 0, .5);
   color: #fff;
   font-size: 1.2em;
   border: none;
-  border-radius: 3px;
-  padding: .5em;
   width: 200px;
   margin: 0 auto;
 }
 
 .room-invitation-overlay .btn-group {
   position: absolute;
   bottom: 10px;
 }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -260,16 +260,24 @@ loop.shared.actions = (function() {
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     RenameRoom: Action.define("renameRoom", {
       roomToken: String,
       newRoomName: String
     }),
 
     /**
+     * Renaming a room error.
+     * XXX: should move to some roomActions module - refs bug 1079284
+     */
+    RenameRoomError: Action.define("renameRoomError", {
+      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", {
       roomUrl: String
     }),
 
     /**
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -93,16 +93,17 @@ loop.store = loop.store || {};
       "copyRoomUrl",
       "deleteRoom",
       "deleteRoomError",
       "emailRoomUrl",
       "getAllRooms",
       "getAllRoomsError",
       "openRoom",
       "renameRoom",
+      "renameRoomError",
       "updateRoomList"
     ],
 
     initialize: function(options) {
       if (!options.mozLoop) {
         throw new Error("Missing option mozLoop");
       }
       this._mozLoop = options.mozLoop;
@@ -115,17 +116,17 @@ loop.store = loop.store || {};
     },
 
     getInitialStoreState: function() {
       return {
         activeRoom: this.activeRoomStore ? this.activeRoomStore.getStoreState() : {},
         error: null,
         pendingCreation: false,
         pendingInitialRetrieval: false,
-        rooms: []
+        rooms: [],
       };
     },
 
     /**
      * Registers mozLoop.rooms events.
      */
     startListeningToRoomEvents: function() {
       // Rooms event registration
@@ -375,18 +376,22 @@ loop.store = loop.store || {};
     },
 
     /**
      * Renames a room.
      *
      * @param {sharedActions.RenameRoom} actionData
      */
     renameRoom: function(actionData) {
+      this.setStoreState({error: null});
       this._mozLoop.rooms.rename(actionData.roomToken, actionData.newRoomName,
         function(err) {
           if (err) {
-            // XXX Give this a proper UI - bug 1100595.
-            console.error("Failed to rename the room", err);
+            this.dispatchAction(new sharedActions.RenameRoomError({error: err}));
           }
-        });
+        }.bind(this));
+    },
+
+    renameRoomError: function(actionData) {
+      this.setStoreState({error: actionData.error});
     }
   });
 })();
--- a/browser/components/loop/test/shared/roomStore_test.js
+++ b/browser/components/loop/test/shared/roomStore_test.js
@@ -77,16 +77,17 @@ describe("loop.store.RoomStore", functio
     beforeEach(function() {
       fakeMozLoop = {
         copyString: function() {},
         notifyUITour: function() {},
         rooms: {
           create: function() {},
           getAll: function() {},
           open: function() {},
+          rename: function() {},
           on: sandbox.stub()
         }
       };
       store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
       store.setStoreState(defaultStoreState);
     });
 
     describe("MozLoop rooms event listeners", function() {
@@ -429,26 +430,41 @@ describe("loop.store.RoomStore", functio
   });
 
   describe("#renameRoom", function() {
     var store, fakeMozLoop;
 
     beforeEach(function() {
       fakeMozLoop = {
         rooms: {
-          rename: sinon.spy()
+          rename: null
         }
       };
       store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
     });
 
     it("should rename the room via mozLoop", function() {
+      fakeMozLoop.rooms.rename = sinon.spy();
       dispatcher.dispatch(new sharedActions.RenameRoom({
         roomToken: "42abc",
         newRoomName: "silly name"
       }));
 
       sinon.assert.calledOnce(fakeMozLoop.rooms.rename);
       sinon.assert.calledWith(fakeMozLoop.rooms.rename, "42abc",
         "silly name");
     });
+
+    it("should store any rename-encountered error", function() {
+      var err = new Error("fake");
+      sandbox.stub(fakeMozLoop.rooms, "rename", function(roomToken, roomName, cb) {
+        cb(err);
+      });
+
+      dispatcher.dispatch(new sharedActions.RenameRoom({
+        roomToken: "42abc",
+        newRoomName: "silly name"
+      }));
+
+      expect(store.getStoreState().error).eql(err);
+    });
   });
 });