Bug 1102170 - Share a room url by email when Loop direct call fails. r=Standard8
authorNicolas Perriault <nperriault@mozilla.com>
Wed, 10 Dec 2014 22:51:53 +0100
changeset 219105 46bf7d2c4ca536fab25691eac5e82d1040bf81dd
parent 219097 0cf461e62ce5008be00beb1d79f300c264119430
child 219106 a1bfd08797630df51d1d274d3f4d39a52cc66a52
push id27956
push userkwierso@gmail.com
push dateFri, 12 Dec 2014 00:47:19 +0000
treeherdermozilla-central@32a2c5bd2f68 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs1102170
milestone37.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 1102170 - Share a room url by email when Loop direct call fails. r=Standard8
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/js/actions.js
browser/components/loop/content/shared/js/conversationStore.js
browser/components/loop/content/shared/js/roomStore.js
browser/components/loop/test/desktop-local/conversationViews_test.js
browser/components/loop/test/shared/conversationStore_test.js
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -22,16 +22,23 @@ loop.conversationViews = (function(mozL1
   function _getPreferredEmail(contact) {
     // A contact may not contain email addresses, but only a phone number.
     if (!contact.email || contact.email.length === 0) {
       return { value: "" };
     }
     return contact.email.find(e => e.pref) || contact.email[0];
   }
 
+  function _getContactDisplayName(contact) {
+    if (contact.name && contact.name[0]) {
+      return contact.name[0];
+    }
+    return _getPreferredEmail(contact).value;
+  }
+
   /**
    * Displays information about the call
    * Caller avatar, name & conversation creation date
    */
   var CallIdentifierView = React.createClass({displayName: 'CallIdentifierView',
     propTypes: {
       peerIdentifier: React.PropTypes.string,
       showIcons: React.PropTypes.bool.isRequired,
@@ -102,24 +109,17 @@ loop.conversationViews = (function(mozL1
    * via children properties.
    */
   var ConversationDetailView = React.createClass({displayName: 'ConversationDetailView',
     propTypes: {
       contact: React.PropTypes.object
     },
 
     render: function() {
-      var contactName;
-
-      if (this.props.contact.name &&
-          this.props.contact.name[0]) {
-        contactName = this.props.contact.name[0];
-      } else {
-        contactName = _getPreferredEmail(this.props.contact).value;
-      }
+      var contactName = _getContactDisplayName(this.props.contact);
 
       document.title = contactName;
 
       return (
         React.DOM.div({className: "call-window"}, 
           CallIdentifierView({
             peerIdentifier: contactName, 
             showIcons: false}), 
@@ -257,17 +257,20 @@ loop.conversationViews = (function(mozL1
     },
 
     emailLink: function() {
       this.setState({
         emailLinkError: false,
         emailLinkButtonDisabled: true
       });
 
-      this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+      this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
+        roomOwner: navigator.mozLoop.userProfile.email,
+        roomName: _getContactDisplayName(this.props.contact)
+      }));
     },
 
     render: function() {
       return (
         React.DOM.div({className: "call-window"}, 
           React.DOM.h2(null, mozL10n.get("generic_failure_title")), 
 
           React.DOM.p({className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")), 
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -22,16 +22,23 @@ loop.conversationViews = (function(mozL1
   function _getPreferredEmail(contact) {
     // A contact may not contain email addresses, but only a phone number.
     if (!contact.email || contact.email.length === 0) {
       return { value: "" };
     }
     return contact.email.find(e => e.pref) || contact.email[0];
   }
 
+  function _getContactDisplayName(contact) {
+    if (contact.name && contact.name[0]) {
+      return contact.name[0];
+    }
+    return _getPreferredEmail(contact).value;
+  }
+
   /**
    * Displays information about the call
    * Caller avatar, name & conversation creation date
    */
   var CallIdentifierView = React.createClass({
     propTypes: {
       peerIdentifier: React.PropTypes.string,
       showIcons: React.PropTypes.bool.isRequired,
@@ -102,24 +109,17 @@ loop.conversationViews = (function(mozL1
    * via children properties.
    */
   var ConversationDetailView = React.createClass({
     propTypes: {
       contact: React.PropTypes.object
     },
 
     render: function() {
-      var contactName;
-
-      if (this.props.contact.name &&
-          this.props.contact.name[0]) {
-        contactName = this.props.contact.name[0];
-      } else {
-        contactName = _getPreferredEmail(this.props.contact).value;
-      }
+      var contactName = _getContactDisplayName(this.props.contact);
 
       document.title = contactName;
 
       return (
         <div className="call-window">
           <CallIdentifierView
             peerIdentifier={contactName}
             showIcons={false} />
@@ -257,17 +257,20 @@ loop.conversationViews = (function(mozL1
     },
 
     emailLink: function() {
       this.setState({
         emailLinkError: false,
         emailLinkButtonDisabled: true
       });
 
-      this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink());
+      this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
+        roomOwner: navigator.mozLoop.userProfile.email,
+        roomName: _getContactDisplayName(this.props.contact)
+      }));
     },
 
     render: function() {
       return (
         <div className="call-window">
           <h2>{mozL10n.get("generic_failure_title")}</h2>
 
           <p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -71,20 +71,22 @@ loop.shared.actions = (function() {
 
     /**
      * Used to signal when the window is being unloaded.
      */
     WindowUnload: Action.define("windowUnload", {
     }),
 
     /**
-     * Fetch a new call url from the server, intended to be sent over email when
+     * Fetch a new room url from the server, intended to be sent over email when
      * a contact can't be reached.
      */
-    FetchEmailLink: Action.define("fetchEmailLink", {
+    FetchRoomEmailLink: Action.define("fetchRoomEmailLink", {
+      roomOwner: String,
+      roomName: String
     }),
 
     /**
      * Used to cancel call setup.
      */
     CancelCall: Action.define("cancelCall", {
     }),
 
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -205,17 +205,17 @@ loop.store = loop.store || {};
         "connectionProgress",
         "connectCall",
         "hangupCall",
         "remotePeerDisconnected",
         "cancelCall",
         "retryCall",
         "mediaConnected",
         "setMute",
-        "fetchEmailLink"
+        "fetchRoomEmailLink"
       ]);
 
       this.setStoreState({
         contact: actionData.contact,
         outgoing: windowType === "outgoing",
         windowId: actionData.windowId,
         callType: actionData.callType,
         callState: CALL_STATES.GATHER,
@@ -318,28 +318,31 @@ loop.store = loop.store || {};
      */
     setMute: function(actionData) {
       var newState = {};
       newState[actionData.type + "Muted"] = !actionData.enabled;
       this.setStoreState(newState);
     },
 
     /**
-     * Fetches a new call URL intended to be sent over email when a contact
+     * Fetches a new room URL intended to be sent over email when a contact
      * can't be reached.
      */
-    fetchEmailLink: function() {
-      // XXX This is an empty string as a conversation identifier. Bug 1015938 implements
-      // a user-set string.
-      this.client.requestCallUrl("", function(err, callUrlData) {
+    fetchRoomEmailLink: function(actionData) {
+      this.mozLoop.rooms.create({
+        roomName: actionData.roomName,
+        roomOwner: actionData.roomOwner,
+        maxSize:   loop.store.MAX_ROOM_CREATION_SIZE,
+        expiresIn: loop.store.DEFAULT_EXPIRES_IN
+      }, function(err, createdRoomData) {
         if (err) {
           this.trigger("error:emailLink");
           return;
         }
-        this.setStoreState({"emailLink": callUrlData.callUrl});
+        this.setStoreState({"emailLink": createdRoomData.roomUrl});
       }.bind(this));
     },
 
     /**
      * Obtains the outgoing call data from the server and handles the
      * result.
      */
     _setupOutgoingCall: function() {
--- a/browser/components/loop/content/shared/js/roomStore.js
+++ b/browser/components/loop/content/shared/js/roomStore.js
@@ -12,16 +12,30 @@ loop.store = loop.store || {};
 
   /**
    * Shared actions.
    * @type {Object}
    */
   var sharedActions = loop.shared.actions;
 
   /**
+   * Maximum size given to createRoom; only 2 is supported (and is
+   * always passed) because that's what the user-experience is currently
+   * designed and tested to handle.
+   * @type {Number}
+   */
+  var MAX_ROOM_CREATION_SIZE = loop.store.MAX_ROOM_CREATION_SIZE = 2;
+
+  /**
+   * The number of hours for which the room will exist - default 8 weeks
+   * @type {Number}
+   */
+  var DEFAULT_EXPIRES_IN = loop.store.DEFAULT_EXPIRES_IN = 24 * 7 * 8;
+
+  /**
    * Room validation schema. See validate.js.
    * @type {Object}
    */
   var roomSchema = {
     roomToken:    String,
     roomUrl:      String,
     roomName:     String,
     maxSize:      Number,
@@ -56,23 +70,23 @@ loop.store = loop.store || {};
    */
   loop.store.RoomStore = loop.store.createStore({
     /**
      * Maximum size given to createRoom; only 2 is supported (and is
      * always passed) because that's what the user-experience is currently
      * designed and tested to handle.
      * @type {Number}
      */
-    maxRoomCreationSize: 2,
+    maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
 
     /**
      * The number of hours for which the room will exist - default 8 weeks
      * @type {Number}
      */
-    defaultExpiresIn: 24 * 7 * 8,
+    defaultExpiresIn: DEFAULT_EXPIRES_IN,
 
     /**
      * Registered actions.
      * @type {Array}
      */
     actions: [
       "createRoom",
       "createRoomError",
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -51,17 +51,20 @@ describe("loop.conversationViews", funct
         return {
           version: "42",
           channel: "test",
           platform: "test"
         };
       },
       getAudioBlob: sinon.spy(function(name, callback) {
         callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
-      })
+      }),
+      userProfile: {
+        email: "bob@invalid.tld"
+      }
     };
 
     fakeWindow = {
       navigator: { mozLoop: fakeMozLoop },
       close: sandbox.stub(),
     };
     loop.shared.mixins.setRootObject(fakeWindow);
 
@@ -236,22 +239,25 @@ describe("loop.conversationViews", funct
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
   });
 
   describe("CallFailedView", function() {
     var store, fakeAudio;
 
-    function mountTestComponent(props) {
+    var contact = {email: [{value: "test@test.tld"}]};
+
+    function mountTestComponent(options) {
+      options = options || {};
       return TestUtils.renderIntoDocument(
         loop.conversationViews.CallFailedView({
           dispatcher: dispatcher,
           store: store,
-          contact: {email: [{value: "test@test.tld"}]}
+          contact: options.contact
         }));
     }
 
     beforeEach(function() {
       store = new loop.store.ConversationStore(dispatcher, {
         client: {},
         mozLoop: navigator.mozLoop,
         sdkDriver: {}
@@ -261,101 +267,121 @@ describe("loop.conversationViews", funct
         pause: sinon.spy(),
         removeAttribute: sinon.spy()
       };
       sandbox.stub(window, "Audio").returns(fakeAudio);
     });
 
     it("should dispatch a retryCall action when the retry button is pressed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         var retryBtn = view.getDOMNode().querySelector('.btn-retry');
 
         React.addons.TestUtils.Simulate.click(retryBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "retryCall"));
       });
 
     it("should dispatch a cancelCall action when the cancel button is pressed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
 
         React.addons.TestUtils.Simulate.click(cancelBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
           sinon.match.hasOwn("name", "cancelCall"));
       });
 
-    it("should dispatch a fetchEmailLink action when the cancel button is pressed",
+    it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
 
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         sinon.assert.calledOnce(dispatcher.dispatch);
         sinon.assert.calledWithMatch(dispatcher.dispatch,
-          sinon.match.hasOwn("name", "fetchEmailLink"));
+          sinon.match.hasOwn("name", "fetchRoomEmailLink"));
+        sinon.assert.calledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("roomOwner", fakeMozLoop.userProfile.email));
+        sinon.assert.calledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("roomName", "test@test.tld"));
+      });
+
+    it("should name the created room using the contact name when available",
+      function() {
+        view = mountTestComponent({contact: {
+          email: [{value: "test@test.tld"}],
+          name: ["Mr Fake ContactName"]
+        }});
+
+        var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
+
+        React.addons.TestUtils.Simulate.click(emailLinkBtn);
+
+        sinon.assert.calledOnce(dispatcher.dispatch);
+        sinon.assert.calledWithMatch(dispatcher.dispatch,
+          sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
       });
 
     it("should disable the email link button once the action is dispatched",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
         var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
         React.addons.TestUtils.Simulate.click(emailLinkBtn);
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(true);
       });
 
     it("should compose an email once the email link is received", function() {
       var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
-      view = mountTestComponent();
+      view = mountTestComponent({contact: contact});
       store.setStoreState({emailLink: "http://fake.invalid/"});
 
       sinon.assert.calledOnce(composeCallUrlEmail);
       sinon.assert.calledWithExactly(composeCallUrlEmail,
         "http://fake.invalid/", "test@test.tld");
     });
 
     it("should close the conversation window once the email link is received",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         store.setStoreState({emailLink: "http://fake.invalid/"});
 
         sinon.assert.calledOnce(fakeWindow.close);
       });
 
     it("should display an error message in case email link retrieval failed",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         store.trigger("error:emailLink");
 
         expect(view.getDOMNode().querySelector(".error")).not.eql(null);
       });
 
     it("should allow retrying to get a call url if it failed previously",
       function() {
-        view = mountTestComponent();
+        view = mountTestComponent({contact: contact});
 
         store.trigger("error:emailLink");
 
         expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
       });
 
     it("should play a failure sound, once", function() {
-      view = mountTestComponent();
+      view = mountTestComponent({contact: contact});
 
       sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
       sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,
                                      "failure", sinon.match.func);
       sinon.assert.calledOnce(fakeAudio.play);
       expect(fakeAudio.loop).to.equal(false);
     });
   });
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -37,16 +37,19 @@ describe("loop.store.ConversationStore",
     };
 
     fakeMozLoop = {
       getLoopPref: sandbox.stub(),
       addConversationContext: sandbox.stub(),
       calls: {
         setCallInProgress: sandbox.stub(),
         clearCallInProgress: sandbox.stub()
+      },
+      rooms: {
+        create: sandbox.stub()
       }
     };
 
     dispatcher = new loop.Dispatcher();
     client = {
       setupOutgoingCall: sinon.stub(),
       requestCallUrl: sinon.stub()
     };
@@ -696,41 +699,53 @@ describe("loop.store.ConversationStore",
         type: "video",
         enabled: false
       }));
 
       expect(store.getStoreState("videoMuted")).eql(true);
     });
   });
 
-  describe("#fetchEmailLink", function() {
+  describe("#fetchRoomEmailLink", function() {
     it("should request a new call url to the server", function() {
-      store.fetchEmailLink(new sharedActions.FetchEmailLink());
+      store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+        roomOwner: "bob@invalid.tld",
+        roomName: "FakeRoomName"
+      }));
 
-      sinon.assert.calledOnce(client.requestCallUrl);
-      sinon.assert.calledWith(client.requestCallUrl, "");
+      sinon.assert.calledOnce(fakeMozLoop.rooms.create);
+      sinon.assert.calledWithMatch(fakeMozLoop.rooms.create, {
+        roomOwner: "bob@invalid.tld",
+        roomName: "FakeRoomName"
+      });
     });
 
-    it("should update the emailLink attribute when the new call url is received",
+    it("should update the emailLink attribute when the new room url is received",
       function() {
-        client.requestCallUrl = function(callId, cb) {
-          cb(null, {callUrl: "http://fake.invalid/"});
+        fakeMozLoop.rooms.create = function(roomData, cb) {
+          cb(null, {roomUrl: "http://fake.invalid/"});
         };
-        store.fetchEmailLink(new sharedActions.FetchEmailLink());
+        store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+          roomOwner: "bob@invalid.tld",
+          roomName: "FakeRoomName"
+        }));
 
         expect(store.getStoreState("emailLink")).eql("http://fake.invalid/");
       });
 
     it("should trigger an error:emailLink event in case of failure",
       function() {
         var trigger = sandbox.stub(store, "trigger");
-        client.requestCallUrl = function(callId, cb) {
-          cb("error");
+        fakeMozLoop.rooms.create = function(roomData, cb) {
+          cb(new Error("error"));
         };
-        store.fetchEmailLink(new sharedActions.FetchEmailLink());
+        store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
+          roomOwner: "bob@invalid.tld",
+          roomName: "FakeRoomName"
+        }));
 
         sinon.assert.calledOnce(trigger);
         sinon.assert.calledWithExactly(trigger, "error:emailLink");
       });
   });
 
   describe("Events", function() {
     describe("Websocket progress", function() {