Bug 1118061-add "caller unavailable" messages to Hello, r=jaws
authorDan Mosedale <dmose@meer.net>
Fri, 09 Jan 2015 14:51:51 -0800
changeset 249033 1d30c40f6e7a97f2c49acdd0173916346a46ec7f
parent 249032 2c169c448081ad0ba4911e3d5c5851a3f2d3b581
child 249034 8fe5a1d4052e3e736c891ad542597f6158520301
push id4489
push userraliiev@mozilla.com
push dateMon, 23 Feb 2015 15:17:55 +0000
treeherdermozilla-beta@fd7c3dc24146 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersjaws
bugs1118061
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 1118061-add "caller unavailable" messages to Hello, r=jaws
browser/components/loop/content/js/conversationViews.js
browser/components/loop/content/js/conversationViews.jsx
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/test/desktop-local/conversationViews_test.js
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/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -15,16 +15,21 @@ loop.conversationViews = (function(mozL1
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
+
+  // XXXdmose this code is already out of sync with the code in contacts.jsx
+  // which, unlike this code, now has unit tests.  We should totally do the
+  // above.
+
   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];
   }
 
@@ -756,16 +761,35 @@ loop.conversationViews = (function(mozL1
 
     _renderError: function() {
       if (!this.state.emailLinkError) {
         return;
       }
       return React.createElement("p", {className: "error"}, mozL10n.get("unable_retrieve_url"));
     },
 
+    _getTitleMessage: function() {
+      var callStateReason =
+        this.props.store.getStoreState("callStateReason");
+
+      if (callStateReason === "reject" || callStateReason === "busy" ||
+          callStateReason === "setup") {
+        var contactDisplayName = _getContactDisplayName(this.props.contact);
+        if (contactDisplayName.length) {
+          return mozL10n.get(
+            "contact_unavailable_title",
+            {"contactName": contactDisplayName});
+        }
+
+        return mozL10n.get("generic_contact_unavailable_title");
+      } else {
+        return mozL10n.get("generic_failure_title");
+      }
+    },
+
     retryCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.RetryCall());
     },
 
     cancelCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.CancelCall());
     },
 
@@ -779,17 +803,17 @@ loop.conversationViews = (function(mozL1
         roomOwner: navigator.mozLoop.userProfile.email,
         roomName: _getContactDisplayName(this.props.contact)
       }));
     },
 
     render: function() {
       return (
         React.createElement("div", {className: "call-window"}, 
-          React.createElement("h2", null, mozL10n.get("generic_failure_title")), 
+          React.createElement("h2", null,  this._getTitleMessage() ), 
 
           React.createElement("p", {className: "btn-label"}, mozL10n.get("generic_failure_with_reason2")), 
 
           this._renderError(), 
 
           React.createElement("div", {className: "btn-group call-action-group"}, 
             React.createElement("button", {className: "btn btn-cancel", 
                     onClick: this.cancelCall}, 
@@ -1043,16 +1067,17 @@ loop.conversationViews = (function(mozL1
     },
   });
 
   return {
     PendingConversationView: PendingConversationView,
     CallIdentifierView: CallIdentifierView,
     ConversationDetailView: ConversationDetailView,
     CallFailedView: CallFailedView,
+    _getContactDisplayName: _getContactDisplayName,
     GenericFailureView: GenericFailureView,
     IncomingCallView: IncomingCallView,
     IncomingConversationView: IncomingConversationView,
     OngoingConversationView: OngoingConversationView,
     OutgoingConversationView: OutgoingConversationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -15,16 +15,21 @@ loop.conversationViews = (function(mozL1
   var sharedUtils = loop.shared.utils;
   var sharedViews = loop.shared.views;
   var sharedMixins = loop.shared.mixins;
   var sharedModels = loop.shared.models;
 
   // This duplicates a similar function in contacts.jsx that isn't used in the
   // conversation window. If we get too many of these, we might want to consider
   // finding a logical place for them to be shared.
+
+  // XXXdmose this code is already out of sync with the code in contacts.jsx
+  // which, unlike this code, now has unit tests.  We should totally do the
+  // above.
+
   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];
   }
 
@@ -756,16 +761,35 @@ loop.conversationViews = (function(mozL1
 
     _renderError: function() {
       if (!this.state.emailLinkError) {
         return;
       }
       return <p className="error">{mozL10n.get("unable_retrieve_url")}</p>;
     },
 
+    _getTitleMessage: function() {
+      var callStateReason =
+        this.props.store.getStoreState("callStateReason");
+
+      if (callStateReason === "reject" || callStateReason === "busy" ||
+          callStateReason === "setup") {
+        var contactDisplayName = _getContactDisplayName(this.props.contact);
+        if (contactDisplayName.length) {
+          return mozL10n.get(
+            "contact_unavailable_title",
+            {"contactName": contactDisplayName});
+        }
+
+        return mozL10n.get("generic_contact_unavailable_title");
+      } else {
+        return mozL10n.get("generic_failure_title");
+      }
+    },
+
     retryCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.RetryCall());
     },
 
     cancelCall: function() {
       this.props.dispatcher.dispatch(new sharedActions.CancelCall());
     },
 
@@ -779,17 +803,17 @@ loop.conversationViews = (function(mozL1
         roomOwner: navigator.mozLoop.userProfile.email,
         roomName: _getContactDisplayName(this.props.contact)
       }));
     },
 
     render: function() {
       return (
         <div className="call-window">
-          <h2>{mozL10n.get("generic_failure_title")}</h2>
+          <h2>{ this._getTitleMessage() }</h2>
 
           <p className="btn-label">{mozL10n.get("generic_failure_with_reason2")}</p>
 
           {this._renderError()}
 
           <div className="btn-group call-action-group">
             <button className="btn btn-cancel"
                     onClick={this.cancelCall}>
@@ -1043,16 +1067,17 @@ loop.conversationViews = (function(mozL1
     },
   });
 
   return {
     PendingConversationView: PendingConversationView,
     CallIdentifierView: CallIdentifierView,
     ConversationDetailView: ConversationDetailView,
     CallFailedView: CallFailedView,
+    _getContactDisplayName: _getContactDisplayName,
     GenericFailureView: GenericFailureView,
     IncomingCallView: IncomingCallView,
     IncomingConversationView: IncomingConversationView,
     OngoingConversationView: OngoingConversationView,
     OutgoingConversationView: OutgoingConversationView
   };
 
 })(document.mozL10n || navigator.mozL10n);
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -274,17 +274,17 @@
 }
 
 /* XXX Once we get the incoming call avatar, bug 1047435, the H2 should
  * disappear from our markup, and we should remove this rule entirely.
  */
 .call-window h2 {
   font-size: 1.5em;
   font-weight: normal;
-
+  text-align: center;
   /* compensate for reset.css overriding this; values borrowed from
      Firefox Mac html.css */
   margin: 0.83em 0;
 }
 
 .fx-embedded-call-button-spacer {
   display: flex;
   flex: 1;
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -407,16 +407,71 @@ describe("loop.conversationViews", funct
       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);
     });
+
+    it("should show 'something went wrong' when the reason is 'media-fail'",
+      function () {
+        store.setStoreState({callStateReason: "media-fail"});
+
+        view = mountTestComponent({contact: contact});
+
+        sinon.assert.calledWith(document.mozL10n.get, "generic_failure_title");
+      });
+
+    it("should show 'contact unavailable' when the reason is 'reject'",
+      function () {
+        store.setStoreState({callStateReason: "reject"});
+
+        view = mountTestComponent({contact: contact});
+
+        sinon.assert.calledWithExactly(document.mozL10n.get,
+          "contact_unavailable_title",
+          {contactName: loop.conversationViews._getContactDisplayName(contact)});
+      });
+
+    it("should show 'contact unavailable' when the reason is 'busy'",
+      function () {
+        store.setStoreState({callStateReason: "busy"});
+
+        view = mountTestComponent({contact: contact});
+
+        sinon.assert.calledWithExactly(document.mozL10n.get,
+          "contact_unavailable_title",
+          {contactName: loop.conversationViews._getContactDisplayName(contact)});
+      });
+
+    it("should show 'contact unavailable' when the reason is 'setup'",
+      function () {
+        store.setStoreState({callStateReason: "setup"});
+
+        view = mountTestComponent({contact: contact});
+
+        sinon.assert.calledWithExactly(document.mozL10n.get,
+          "contact_unavailable_title",
+          {contactName: loop.conversationViews._getContactDisplayName(contact)});
+      });
+
+    it("should display a generic contact unavailable msg when the reason is" +
+       " 'busy' and no display name is available", function() {
+        store.setStoreState({callStateReason: "busy"});
+        var phoneOnlyContact = {
+          tel: [{"pref": true, type: "work", value: ""}]
+        };
+
+        view = mountTestComponent({contact: phoneOnlyContact});
+
+        sinon.assert.calledWith(document.mozL10n.get,
+          "generic_contact_unavailable_title");
+    });
   });
 
   describe("OngoingConversationView", function() {
     function mountTestComponent(props) {
       return TestUtils.renderIntoDocument(
         React.createElement(loop.conversationViews.OngoingConversationView, props));
     }
 
@@ -526,17 +581,20 @@ describe("loop.conversationViews", funct
       });
       feedbackStore = new loop.store.FeedbackStore(dispatcher, {
         feedbackClient: {}
       });
     });
 
     it("should render the CallFailedView when the call state is 'terminated'",
       function() {
-        store.setStoreState({callState: CALL_STATES.TERMINATED});
+        store.setStoreState({
+          callState: CALL_STATES.TERMINATED,
+          contact: contact
+        });
 
         view = mountTestComponent();
 
         TestUtils.findRenderedComponentWithType(view,
           loop.conversationViews.CallFailedView);
     });
 
     it("should render the PendingConversationView when the call state is 'gather'",
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -69,16 +69,21 @@
     sdkDriver: {}
   });
   var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop
   });
   var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
     feedbackClient: stageFeedbackApiClient
   });
+  var conversationStore = new loop.store.ConversationStore(dispatcher, {
+    client: {},
+    mozLoop: navigator.mozLoop,
+    sdkDriver: {}
+  });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
   mockMozLoopRooms.roomsEnabled = true;
 
   var mockContact = {
     name: ["Mr Smith"],
@@ -371,23 +376,24 @@
               )
             )
           ), 
 
           React.createElement(Section, {name: "CallFailedView"}, 
             React.createElement(Example, {summary: "Call Failed", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(CallFailedView, {dispatcher: dispatcher})
+                React.createElement(CallFailedView, {dispatcher: dispatcher, store: conversationStore})
               )
             ), 
             React.createElement(Example, {summary: "Call Failed — with call URL error", dashed: "true", 
                      style: {width: "260px", height: "265px"}}, 
               React.createElement("div", {className: "fx-embedded"}, 
-                React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true})
+                React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true, 
+                                store: conversationStore})
               )
             )
           ), 
 
           React.createElement(Section, {name: "StartConversationView"}, 
             React.createElement(Example, {summary: "Start conversation view", dashed: "true"}, 
               React.createElement("div", {className: "standalone"}, 
                 React.createElement(StartConversationView, {conversation: mockConversationModel, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -69,16 +69,21 @@
     sdkDriver: {}
   });
   var roomStore = new loop.store.RoomStore(dispatcher, {
     mozLoop: navigator.mozLoop
   });
   var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
     feedbackClient: stageFeedbackApiClient
   });
+  var conversationStore = new loop.store.ConversationStore(dispatcher, {
+    client: {},
+    mozLoop: navigator.mozLoop,
+    sdkDriver: {}
+  });
 
   // Local mocks
 
   var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
   mockMozLoopRooms.roomsEnabled = true;
 
   var mockContact = {
     name: ["Mr Smith"],
@@ -371,23 +376,24 @@
               </div>
             </Example>
           </Section>
 
           <Section name="CallFailedView">
             <Example summary="Call Failed" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
-                <CallFailedView dispatcher={dispatcher} />
+                <CallFailedView dispatcher={dispatcher} store={conversationStore} />
               </div>
             </Example>
             <Example summary="Call Failed — with call URL error" dashed="true"
                      style={{width: "260px", height: "265px"}}>
               <div className="fx-embedded">
-                <CallFailedView dispatcher={dispatcher} emailLinkError={true} />
+                <CallFailedView dispatcher={dispatcher} emailLinkError={true}
+                                store={conversationStore} />
               </div>
             </Example>
           </Section>
 
           <Section name="StartConversationView">
             <Example summary="Start conversation view" dashed="true">
               <div className="standalone">
                 <StartConversationView conversation={mockConversationModel}
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -220,16 +220,22 @@ call_progress_connecting_description=Connecting…
 ## when the other client is actually ringing.
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#call-outgoing
 call_progress_ringing_description=Ringing…
 
 peer_ended_conversation2=The person you were calling has ended the conversation.
 conversation_has_ended=Your conversation has ended.
 restart_call=Rejoin
 
+## LOCALIZATION NOTE (contact_unavailable_title): The title displayed
+## when a contact is unavailable. Don't translate the part between {{..}}
+## because this will be replaced by the contact's name.
+contact_unavailable_title={{contactName}} is unavailable.
+generic_contact_unavailable_title=This person is unavailable.
+
 generic_failure_title=Something went wrong.
 generic_failure_with_reason2=You can try again or email a link to be reached at later.
 generic_failure_no_reason2=Would you like to try again?
 
 ## LOCALIZATION NOTE (contact_offline_title): Title for
 ## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#link-prompt
 ## displayed when the contact is offline.
 contact_offline_title=This person is not online