Bug 1142588 - Implement context in conversations display for Loop's standalone UI. r=mikedeboer
authorMark Banner <standard8@mozilla.com>
Wed, 15 Apr 2015 13:06:34 +0100
changeset 239326 7b002a2e4932f5e5019e77834ce94bdbd7cbd2ff
parent 239325 3570dbae06e2c9206a7d99709c209bacc56b537b
child 239327 cdf2a0056d6abc01235ffd35a7b02e91fbbccc30
push id28589
push userryanvm@gmail.com
push dateWed, 15 Apr 2015 19:13:10 +0000
treeherdermozilla-central@24ccca4707eb [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1142588
milestone40.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1142588 - Implement context in conversations display for Loop's standalone UI. r=mikedeboer
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/activeRoomStore.js
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/test/shared/activeRoomStore_test.js
browser/components/loop/test/standalone/standaloneRoomViews_test.js
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -1027,28 +1027,54 @@ body[dir=rtl] .share-service-dropdown .s
 
 .standalone .room-conversation-wrapper .room-inner-info-area a.btn {
   padding: .5em 3em .3em 3em;
   border-radius: 3px;
   font-weight: normal;
   max-width: 400px;
 }
 
-.standalone .room-conversation h2.room-name,
-.standalone .room-conversation h2.room-info-failure {
+.standalone-room-info {
   position: absolute;
-  display: inline-block;
+  display: block;
   top: 0;
   right: 10px;
   /* 20px is 10px for left and right margins. */
   width: calc(25% - 20px);
-  color: #fff;
   z-index: 2000000;
   font-size: 1.2em;
   padding: .4em;
+  height: 100%;
+}
+
+.standalone-room-info > h2 {
+  color: #fff;
+}
+
+.standalone-context-url {
+  color: #fff;
+  /* Try and keep clear of local video */
+  height: 40%;
+}
+
+.standalone-context-url.screen-share-active {
+  /* Try and keep clear of remote video when screensharing */
+  height: 15%;
+}
+
+.standalone-context-url > img {
+  margin: 1em auto;
+  max-width: 50%;
+  /* allows 20% for the description wrapper plus the margins */
+  max-height: calc(80% - 2em);
+}
+
+.standalone-context-url-description-wrapper {
+  /* So that we can use max-height for the image */
+  height: 20%;
 }
 
 .standalone .room-conversation .media {
   background: #000;
 }
 
 .standalone .room-conversation .video_wrapper.remote_wrapper {
   background-color: #4e4e4e;
@@ -1066,16 +1092,30 @@ body[dir=rtl] .share-service-dropdown .s
 
 .standalone .room-conversation-wrapper .ended-conversation {
   position: relative;
   height: auto;
 }
 
 
 @media screen and (max-width:640px) {
+  .standalone-room-info {
+    /* This isn't perfect, we just center the heading for now. Bug 1141493
+       should fix this. */
+    position: absolute;
+    width: 100%;
+    right: 0px;
+  }
+
+  .standalone-context-url {
+    /* XXX We haven't got UX for standalone yet, so temporarily not displaying
+       on narrow window widths. See bug 1153827. */
+    display: none;
+  }
+
   /* Rooms specific responsive styling */
   .standalone .room-conversation {
     background: #000;
   }
   .room-conversation-wrapper header {
     width: 100%;
   }
   .standalone .room-conversation-wrapper .room-inner-info-area {
@@ -1086,16 +1126,20 @@ body[dir=rtl] .share-service-dropdown .s
   }
   .standalone .room-conversation-wrapper .video-layout-wrapper {
     /* 50px: header's height; 25px: footer's height */
     height: calc(100% - 50px - 25px);
   }
   .standalone .room-conversation .video_wrapper.remote_wrapper {
     width: 100%;
   }
+  .standalone .room-conversation .video_wrapper.remote_wrapper.not-joined {
+    width: 100%;
+  }
+
   .standalone .conversation-toolbar {
     height: 38px;
     padding: 8px;
   }
   .standalone .focus-stream {
     /* Set at maximum height, minus height of conversation toolbar */
     height: 100%;
   }
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -414,16 +414,18 @@ loop.shared.actions = (function() {
      *
      * @see https://wiki.mozilla.org/Loop/Architecture/Rooms#GET_.2Frooms.2F.7Btoken.7D
      */
     UpdateRoomInfo: Action.define("updateRoomInfo", {
       // context: Object - Optional.
       // roomName: String - Optional.
       roomOwner: String,
       roomUrl: String
+      // urls: Array - Optional.
+      // See https://wiki.mozilla.org/Loop/Architecture/Context#Format_of_context.value
     }),
 
     /**
      * Updates the Social API information when it is received.
      * XXX: should move to some roomActions module - refs bug 1079284
      */
     UpdateSocialShareInfo: Action.define("updateSocialShareInfo", {
       socialShareButtonAvailable: Boolean,
--- a/browser/components/loop/content/shared/js/activeRoomStore.js
+++ b/browser/components/loop/content/shared/js/activeRoomStore.js
@@ -75,20 +75,24 @@ loop.store.ActiveRoomStore = (function()
         // session. 'Used' means at least one call has been placed
         // with it. Entering and leaving the room without seeing
         // anyone is not considered as 'used'
         used: false,
         localVideoDimensions: {},
         remoteVideoDimensions: {},
         screenSharingState: SCREEN_SHARE_STATES.INACTIVE,
         receivingScreenShare: false,
+        // Any urls (aka context) associated with the room.
+        roomContextUrls: null,
         // The roomCryptoKey to decode the context data if necessary.
         roomCryptoKey: null,
         // Room information failed to be obtained for a reason. See ROOM_INFO_FAILURES.
         roomInfoFailure: null,
+        // The name of the room.
+        roomName: null,
         // Social API state.
         socialShareButtonAvailable: false,
         socialShareProviders: null
       };
     },
 
     /**
      * Handles a room failure.
@@ -266,16 +270,17 @@ loop.store.ActiveRoomStore = (function()
         }
 
         var dispatcher = this.dispatcher;
 
         crypto.decryptBytes(roomCryptoKey, result.context.value)
               .then(function(decryptedResult) {
           var realResult = JSON.parse(decryptedResult);
 
+          roomInfoData.urls = realResult.urls;
           roomInfoData.roomName = realResult.roomName;
 
           dispatcher.dispatch(roomInfoData);
         }, function(err) {
           roomInfoData.roomInfoFailure = ROOM_INFO_FAILURES.DECRYPT_FAILED;
           dispatcher.dispatch(roomInfoData);
         });
       }.bind(this));
@@ -315,16 +320,17 @@ loop.store.ActiveRoomStore = (function()
 
     /**
      * Handles the updateRoomInfo action. Updates the room data.
      *
      * @param {sharedActions.UpdateRoomInfo} actionData
      */
     updateRoomInfo: function(actionData) {
       this.setStoreState({
+        roomContextUrls: actionData.urls,
         roomInfoFailure: actionData.roomInfoFailure,
         roomName: actionData.roomName,
         roomOwner: actionData.roomOwner,
         roomUrl: actionData.roomUrl
       });
     },
 
     /**
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -194,35 +194,79 @@ loop.standaloneRoomViews = (function(moz
         React.createElement("footer", null, 
           React.createElement("p", {dangerouslySetInnerHTML: {__html: this._getContent()}}), 
           React.createElement("div", {className: "footer-logo"})
         )
       );
     }
   });
 
+  var StandaloneRoomContextItem = React.createClass({displayName: "StandaloneRoomContextItem",
+    propTypes: {
+      receivingScreenShare: React.PropTypes.bool,
+      roomContextUrl: React.PropTypes.object
+    },
+
+    render: function() {
+      if (!this.props.roomContextUrl ||
+          !this.props.roomContextUrl.location) {
+        return null;
+      }
+
+      var location = this.props.roomContextUrl.location;
+
+      var cx = React.addons.classSet;
+
+      var classes = cx({
+        "standalone-context-url": true,
+        "screen-share-active": this.props.receivingScreenShare
+      });
+
+      return (
+        React.createElement("div", {className: classes}, 
+            React.createElement("img", {src: this.props.roomContextUrl.thumbnail}), 
+          React.createElement("div", {className: "standalone-context-url-description-wrapper"}, 
+            this.props.roomContextUrl.description, 
+            React.createElement("br", null), React.createElement("a", {href: location}, location)
+          )
+        )
+      );
+    }
+  });
+
   var StandaloneRoomContextView = React.createClass({displayName: "StandaloneRoomContextView",
     propTypes: {
+      receivingScreenShare: React.PropTypes.bool.isRequired,
+      roomContextUrls: React.PropTypes.array,
       roomName: React.PropTypes.string,
       roomInfoFailure: React.PropTypes.string
     },
 
     render: function() {
       if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
         return (React.createElement("h2", {className: "room-info-failure"}, 
           mozL10n.get("room_information_failure_unsupported_browser")
         ));
       } else if (this.props.roomInfoFailure) {
         return (React.createElement("h2", {className: "room-info-failure"}, 
           mozL10n.get("room_information_failure_not_available")
         ));
       }
 
+      // We only support one item in the context Urls array for now.
+      var roomContextUrl = (this.props.roomContextUrls &&
+                            this.props.roomContextUrls.length > 0) ?
+                            this.props.roomContextUrls[0] : null;
       return (
-        React.createElement("h2", {className: "room-name"}, this.props.roomName)
+        React.createElement("div", {className: "standalone-room-info"}, 
+          React.createElement("h2", {className: "room-name"}, this.props.roomName), 
+          React.createElement(StandaloneRoomContextItem, {
+            receivingScreenShare: this.props.receivingScreenShare, 
+            roomContextUrl: roomContextUrl})
+        )
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({displayName: "StandaloneRoomView",
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
@@ -477,18 +521,21 @@ loop.standaloneRoomViews = (function(moz
           React.createElement(StandaloneRoomInfoArea, {roomState: this.state.roomState, 
                                   failureReason: this.state.failureReason, 
                                   joinRoom: this.joinRoom, 
                                   isFirefox: this.props.isFirefox, 
                                   activeRoomStore: this.props.activeRoomStore, 
                                   roomUsed: this.state.used}), 
           React.createElement("div", {className: "video-layout-wrapper"}, 
             React.createElement("div", {className: "conversation room-conversation"}, 
-              React.createElement(StandaloneRoomContextView, {roomName: this.state.roomName, 
-                                         roomInfoFailure: this.state.roomInfoFailure}), 
+              React.createElement(StandaloneRoomContextView, {
+                receivingScreenShare: this.state.receivingScreenShare, 
+                roomContextUrls: this.state.roomContextUrls, 
+                roomName: this.state.roomName, 
+                roomInfoFailure: this.state.roomInfoFailure}), 
               React.createElement("div", {className: "media nested"}, 
                 React.createElement("span", {className: "self-view-hidden-message"}, 
                   mozL10n.get("self_view_hidden_message")
                 ), 
                 React.createElement("div", {className: "video_wrapper remote_wrapper"}, 
                   React.createElement("div", {className: remoteStreamClasses}), 
                   React.createElement("div", {className: screenShareStreamClasses})
                 ), 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -194,35 +194,79 @@ loop.standaloneRoomViews = (function(moz
         <footer>
           <p dangerouslySetInnerHTML={{__html: this._getContent()}}></p>
           <div className="footer-logo" />
         </footer>
       );
     }
   });
 
+  var StandaloneRoomContextItem = React.createClass({
+    propTypes: {
+      receivingScreenShare: React.PropTypes.bool,
+      roomContextUrl: React.PropTypes.object
+    },
+
+    render: function() {
+      if (!this.props.roomContextUrl ||
+          !this.props.roomContextUrl.location) {
+        return null;
+      }
+
+      var location = this.props.roomContextUrl.location;
+
+      var cx = React.addons.classSet;
+
+      var classes = cx({
+        "standalone-context-url": true,
+        "screen-share-active": this.props.receivingScreenShare
+      });
+
+      return (
+        <div className={classes}>
+            <img src={this.props.roomContextUrl.thumbnail} />
+          <div className="standalone-context-url-description-wrapper">
+            {this.props.roomContextUrl.description}
+            <br /><a href={location}>{location}</a>
+          </div>
+        </div>
+      );
+    }
+  });
+
   var StandaloneRoomContextView = React.createClass({
     propTypes: {
+      receivingScreenShare: React.PropTypes.bool.isRequired,
+      roomContextUrls: React.PropTypes.array,
       roomName: React.PropTypes.string,
       roomInfoFailure: React.PropTypes.string
     },
 
     render: function() {
       if (this.props.roomInfoFailure === ROOM_INFO_FAILURES.WEB_CRYPTO_UNSUPPORTED) {
         return (<h2 className="room-info-failure">
           {mozL10n.get("room_information_failure_unsupported_browser")}
         </h2>);
       } else if (this.props.roomInfoFailure) {
         return (<h2 className="room-info-failure">
           {mozL10n.get("room_information_failure_not_available")}
         </h2>);
       }
 
+      // We only support one item in the context Urls array for now.
+      var roomContextUrl = (this.props.roomContextUrls &&
+                            this.props.roomContextUrls.length > 0) ?
+                            this.props.roomContextUrls[0] : null;
       return (
-        <h2 className="room-name">{this.props.roomName}</h2>
+        <div className="standalone-room-info">
+          <h2 className="room-name">{this.props.roomName}</h2>
+          <StandaloneRoomContextItem
+            receivingScreenShare={this.props.receivingScreenShare}
+            roomContextUrl={roomContextUrl} />
+        </div>
       );
     }
   });
 
   var StandaloneRoomView = React.createClass({
     mixins: [
       Backbone.Events,
       sharedMixins.MediaSetupMixin,
@@ -477,18 +521,21 @@ loop.standaloneRoomViews = (function(moz
           <StandaloneRoomInfoArea roomState={this.state.roomState}
                                   failureReason={this.state.failureReason}
                                   joinRoom={this.joinRoom}
                                   isFirefox={this.props.isFirefox}
                                   activeRoomStore={this.props.activeRoomStore}
                                   roomUsed={this.state.used} />
           <div className="video-layout-wrapper">
             <div className="conversation room-conversation">
-              <StandaloneRoomContextView roomName={this.state.roomName}
-                                         roomInfoFailure={this.state.roomInfoFailure} />
+              <StandaloneRoomContextView
+                receivingScreenShare={this.state.receivingScreenShare}
+                roomContextUrls={this.state.roomContextUrls}
+                roomName={this.state.roomName}
+                roomInfoFailure={this.state.roomInfoFailure} />
               <div className="media nested">
                 <span className="self-view-hidden-message">
                   {mozL10n.get("self_view_hidden_message")}
                 </span>
                 <div className="video_wrapper remote_wrapper">
                   <div className={remoteStreamClasses}></div>
                   <div className={screenShareStreamClasses}></div>
                 </div>
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -452,36 +452,43 @@ describe("loop.store.ActiveRoomStore", f
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
           new sharedActions.UpdateRoomInfo(_.extend({
             roomInfoFailure: ROOM_INFO_FAILURES.DECRYPT_FAILED
           }, expectedDetails)));
       });
 
-      it("should dispatch UpdateRoomInfo message with the room name if decryption was successful", function() {
+      it("should dispatch UpdateRoomInfo message with the context if decryption was successful", function() {
         fetchServerAction.cryptoKey = "fakeKey";
 
+        var roomContext = {
+          roomName: "The wonderful Loopy room",
+          urls: [{
+            description: "An invalid page",
+            location: "http://invalid.com",
+            thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+          }]
+        };
+
         // This is a work around to turn promise into a sync action to make handling test failures
         // easier.
         sandbox.stub(loop.crypto, "decryptBytes", function() {
           return {
             then: function(resolve, reject) {
-              resolve(JSON.stringify({roomName: "The wonderful Loopy room"}));
+              resolve(JSON.stringify(roomContext));
             }
           };
         });
 
         store.fetchServerData(fetchServerAction);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithExactly(dispatcher.dispatch,
-          new sharedActions.UpdateRoomInfo(_.extend({
-            roomName: "The wonderful Loopy room"
-          }, expectedDetails)));
+          new sharedActions.UpdateRoomInfo(_.extend(roomContext, expectedDetails)));
       });
     });
   });
 
   describe("#feedbackComplete", function() {
     it("should reset the room store state", function() {
       var initialState = store.getInitialStoreState();
       store.setStoreState({
@@ -557,27 +564,33 @@ describe("loop.store.ActiveRoomStore", f
 
   describe("#updateRoomInfo", function() {
     var fakeRoomInfo;
 
     beforeEach(function() {
       fakeRoomInfo = {
         roomName: "Its a room",
         roomOwner: "Me",
-        roomUrl: "http://invalid"
+        roomUrl: "http://invalid",
+        urls: [{
+          description: "fake site",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
       };
     });
 
     it("should save the room information", function() {
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo(fakeRoomInfo));
 
       var state = store.getStoreState();
       expect(state.roomName).eql(fakeRoomInfo.roomName);
       expect(state.roomOwner).eql(fakeRoomInfo.roomOwner);
       expect(state.roomUrl).eql(fakeRoomInfo.roomUrl);
+      expect(state.roomContextUrls).eql(fakeRoomInfo.urls);
     });
   });
 
   describe("#updateSocialShareInfo", function() {
     var fakeSocialShareInfo;
 
     beforeEach(function() {
       fakeSocialShareInfo = {
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -39,25 +39,27 @@ describe("loop.standaloneRoomViews", fun
     sandbox.restore();
   });
 
   describe("StandaloneRoomContextView", function() {
     beforeEach(function() {
       sandbox.stub(navigator.mozL10n, "get").returnsArg(0);
     });
 
-    function mountTestComponent(props) {
+    function mountTestComponent(extraProps) {
+      var props = _.extend({ receivingScreenShare: false }, extraProps);
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomContextView, props));
     }
 
     it("should display the room name if no failures are known", function() {
       var view = mountTestComponent({
-        roomName: "Mike's room"
+        roomName: "Mike's room",
+        receivingScreenShare: false
       });
 
       expect(view.getDOMNode().textContent).eql("Mike's room");
     });
 
     it("should display an unsupported browser message if crypto is unsupported", function() {
       var view = mountTestComponent({
         roomName: "Mark's room",
@@ -70,16 +72,37 @@ describe("loop.standaloneRoomViews", fun
     it("should display a general error message for any other failure", function() {
       var view = mountTestComponent({
         roomName: "Mark's room",
         roomInfoFailure: ROOM_INFO_FAILURES.NO_DATA
       });
 
       expect(view.getDOMNode().textContent).match(/not_available/);
     });
+
+    it("should display context information if a url is supplied", function() {
+      var view = mountTestComponent({
+        roomName: "Mike's room",
+        roomContextUrls: [{
+          description: "Mark's super page",
+          location: "http://invalid.com",
+          thumbnail: "data:image/gif;base64,R0lGODlhAQABAAAAACH5BAEKAAEALAAAAAABAAEAAAICTAEAOw=="
+        }]
+      });
+
+      expect(view.getDOMNode().querySelector(".standalone-context-url")).not.eql(null);
+    });
+
+    it("should not display context information if no urls are supplied", function() {
+      var view = mountTestComponent({
+        roomName: "Mike's room"
+      });
+
+      expect(view.getDOMNode().querySelector(".standalone-context-url")).eql(null);
+    });
   });
 
   describe("StandaloneRoomView", function() {
     function mountTestComponent() {
       return TestUtils.renderIntoDocument(
         React.createElement(
           loop.standaloneRoomViews.StandaloneRoomView, {
             dispatcher: dispatcher,