Bug 1042060 - Implement default answering mode for desktop client. r=mikedeboer
authorAndrei Oprea <andrei.br92@gmail.com>
Wed, 10 Sep 2014 15:50:00 -0400
changeset 225353 4482bccdcceda072594b9d02f48d4fc5f181de30
parent 225352 d4797a01c3a818e4162906c84e0d119d251cd428
child 225354 82528b6a8c8eb0c8edb71da3c5a411b8f546d161
push id3979
push userraliiev@mozilla.com
push dateMon, 13 Oct 2014 16:35:44 +0000
treeherdermozilla-beta@30f2cc610691 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmikedeboer
bugs1042060
milestone34.0a2
Bug 1042060 - Implement default answering mode for desktop client. r=mikedeboer
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/conversation.jsx
browser/components/loop/content/shared/css/common.css
browser/components/loop/content/shared/css/conversation.css
browser/components/loop/test/desktop-local/conversation_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/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -6,34 +6,36 @@
 
 /* jshint newcap:false, esnext:true */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversation = (function(OT, mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views,
-      // aliasing translation function as __ for concision
-      __ = mozL10n.get;
+  var sharedViews = loop.shared.views;
 
   /**
    * App router.
    * @type {loop.desktopRouter.DesktopConversationRouter}
    */
   var router;
 
   var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
 
     propTypes: {
-      model: React.PropTypes.object.isRequired
+      model: React.PropTypes.object.isRequired,
+      video: React.PropTypes.bool.isRequired
     },
 
-    getInitialProps: function() {
-      return {showDeclineMenu: false};
+    getDefaultProps: function() {
+      return {
+        showDeclineMenu: false,
+        video: true
+      };
     },
 
     getInitialState: function() {
       return {showDeclineMenu: this.props.showDeclineMenu};
     },
 
     componentDidMount: function() {
       window.addEventListener("click", this.clickHandler);
@@ -74,84 +76,135 @@ loop.conversation = (function(OT, mozL10
       var currentState = this.state.showDeclineMenu;
       this.setState({showDeclineMenu: !currentState});
     },
 
     _hideDeclineMenu: function() {
       this.setState({showDeclineMenu: false});
     },
 
+    /*
+     * Generate props for <AcceptCallButton> component based on
+     * incoming call type. An incoming video call will render a video
+     * answer button primarily, an audio call will flip them.
+     **/
+    _answerModeProps: function() {
+      var videoButton = {
+        handler: this._handleAccept("audio-video"),
+        className: "fx-embedded-btn-icon-video",
+        tooltip: "incoming_call_accept_audio_video_tooltip"
+      };
+      var audioButton = {
+        handler: this._handleAccept("audio"),
+        className: "fx-embedded-btn-audio-small",
+        tooltip: "incoming_call_accept_audio_only_tooltip"
+      };
+      var props = {};
+      props.primary = videoButton;
+      props.secondary = audioButton;
+
+      // When video is not enabled on this call, we swap the buttons around.
+      if (!this.props.video) {
+        audioButton.className = "fx-embedded-btn-icon-audio";
+        videoButton.className = "fx-embedded-btn-video-small";
+        props.primary = audioButton;
+        props.secondary = videoButton;
+      }
+
+      return props;
+    },
+
     render: function() {
       /* jshint ignore:start */
       var btnClassAccept = "btn btn-accept";
       var btnClassDecline = "btn btn-error btn-decline";
       var conversationPanelClass = "incoming-call";
       var dropdownMenuClassesDecline = React.addons.classSet({
         "native-dropdown-menu": true,
         "conversation-window-dropdown": true,
         "visually-hidden": !this.state.showDeclineMenu
       });
       return (
         React.DOM.div({className: conversationPanelClass}, 
-          React.DOM.h2(null, __("incoming_call_title2")), 
+          React.DOM.h2(null, mozL10n.get("incoming_call_title2")), 
           React.DOM.div({className: "btn-group incoming-call-action-group"}, 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}), 
 
             React.DOM.div({className: "btn-chevron-menu-group"}, 
               React.DOM.div({className: "btn-group-chevron"}, 
                 React.DOM.div({className: "btn-group"}, 
 
                   React.DOM.button({className: btnClassDecline, 
                           onClick: this._handleDecline}, 
-                    __("incoming_call_cancel_button")
+                    mozL10n.get("incoming_call_cancel_button")
                   ), 
                   React.DOM.div({className: "btn-chevron", 
                        onClick: this._toggleDeclineMenu}
                   )
                 ), 
 
                 React.DOM.ul({className: dropdownMenuClassesDecline}, 
                   React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock}, 
-                    __("incoming_call_cancel_and_block_button")
+                    mozL10n.get("incoming_call_cancel_and_block_button")
                   )
                 )
 
               )
             ), 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"}), 
 
-            React.DOM.div({className: "btn-chevron-menu-group"}, 
-              React.DOM.div({className: "btn-group"}, 
-                React.DOM.button({className: btnClassAccept, 
-                        onClick: this._handleAccept("audio-video")}, 
-                  React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
-                    __("incoming_call_accept_button")
-                  ), 
-                  React.DOM.span({className: "fx-embedded-btn-icon-video"}
-                  )
-                ), 
-                React.DOM.div({className: "call-audio-only", 
-                     onClick: this._handleAccept("audio"), 
-                     title: __("incoming_call_accept_audio_only_tooltip")}
-                )
-              )
-            ), 
+            AcceptCallButton({mode: this._answerModeProps()}), 
 
             React.DOM.div({className: "fx-embedded-incoming-call-button-spacer"})
 
           )
         )
       );
       /* jshint ignore:end */
     }
   });
 
   /**
+   * Incoming call view accept button, renders different primary actions
+   * (answer with video / with audio only) based on the props received
+   **/
+  var AcceptCallButton = React.createClass({displayName: 'AcceptCallButton',
+
+    propTypes: {
+      mode: React.PropTypes.object.isRequired,
+    },
+
+    render: function() {
+      var mode = this.props.mode;
+      return (
+        /* jshint ignore:start */
+        React.DOM.div({className: "btn-chevron-menu-group"}, 
+          React.DOM.div({className: "btn-group"}, 
+            React.DOM.button({className: "btn btn-accept", 
+                    onClick: mode.primary.handler, 
+                    title: mozL10n.get(mode.primary.tooltip)}, 
+              React.DOM.span({className: "fx-embedded-answer-btn-text"}, 
+                mozL10n.get("incoming_call_accept_button")
+              ), 
+              React.DOM.span({className: mode.primary.className})
+            ), 
+            React.DOM.div({className: mode.secondary.className, 
+                 onClick: mode.secondary.handler, 
+                 title: mozL10n.get(mode.secondary.tooltip)}
+            )
+          )
+        )
+        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
    * Conversation router.
    *
    * Required options:
    * - {loop.shared.models.ConversationModel} conversation Conversation model.
    * - {loop.shared.models.NotificationCollection} notifications
    *
    * @type {loop.shared.router.BaseConversationRouter}
    */
@@ -220,17 +273,17 @@ loop.conversation = (function(OT, mozL10
       this._websocket = new loop.CallConnectionWebSocket({
         url: this._conversation.get("progressURL"),
         websocketToken: this._conversation.get("websocketToken"),
         callId: this._conversation.get("callId"),
       });
       this._websocket.promiseConnect().then(function() {
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
-          video: {enabled: this._conversation.hasVideoStream("incoming")}
+          video: this._conversation.hasVideoStream("incoming")
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
     },
 
     /**
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -6,34 +6,36 @@
 
 /* jshint newcap:false, esnext:true */
 /* global loop:true, React */
 
 var loop = loop || {};
 loop.conversation = (function(OT, mozL10n) {
   "use strict";
 
-  var sharedViews = loop.shared.views,
-      // aliasing translation function as __ for concision
-      __ = mozL10n.get;
+  var sharedViews = loop.shared.views;
 
   /**
    * App router.
    * @type {loop.desktopRouter.DesktopConversationRouter}
    */
   var router;
 
   var IncomingCallView = React.createClass({
 
     propTypes: {
-      model: React.PropTypes.object.isRequired
+      model: React.PropTypes.object.isRequired,
+      video: React.PropTypes.bool.isRequired
     },
 
-    getInitialProps: function() {
-      return {showDeclineMenu: false};
+    getDefaultProps: function() {
+      return {
+        showDeclineMenu: false,
+        video: true
+      };
     },
 
     getInitialState: function() {
       return {showDeclineMenu: this.props.showDeclineMenu};
     },
 
     componentDidMount: function() {
       window.addEventListener("click", this.clickHandler);
@@ -74,84 +76,135 @@ loop.conversation = (function(OT, mozL10
       var currentState = this.state.showDeclineMenu;
       this.setState({showDeclineMenu: !currentState});
     },
 
     _hideDeclineMenu: function() {
       this.setState({showDeclineMenu: false});
     },
 
+    /*
+     * Generate props for <AcceptCallButton> component based on
+     * incoming call type. An incoming video call will render a video
+     * answer button primarily, an audio call will flip them.
+     **/
+    _answerModeProps: function() {
+      var videoButton = {
+        handler: this._handleAccept("audio-video"),
+        className: "fx-embedded-btn-icon-video",
+        tooltip: "incoming_call_accept_audio_video_tooltip"
+      };
+      var audioButton = {
+        handler: this._handleAccept("audio"),
+        className: "fx-embedded-btn-audio-small",
+        tooltip: "incoming_call_accept_audio_only_tooltip"
+      };
+      var props = {};
+      props.primary = videoButton;
+      props.secondary = audioButton;
+
+      // When video is not enabled on this call, we swap the buttons around.
+      if (!this.props.video) {
+        audioButton.className = "fx-embedded-btn-icon-audio";
+        videoButton.className = "fx-embedded-btn-video-small";
+        props.primary = audioButton;
+        props.secondary = videoButton;
+      }
+
+      return props;
+    },
+
     render: function() {
       /* jshint ignore:start */
       var btnClassAccept = "btn btn-accept";
       var btnClassDecline = "btn btn-error btn-decline";
       var conversationPanelClass = "incoming-call";
       var dropdownMenuClassesDecline = React.addons.classSet({
         "native-dropdown-menu": true,
         "conversation-window-dropdown": true,
         "visually-hidden": !this.state.showDeclineMenu
       });
       return (
         <div className={conversationPanelClass}>
-          <h2>{__("incoming_call_title2")}</h2>
+          <h2>{mozL10n.get("incoming_call_title2")}</h2>
           <div className="btn-group incoming-call-action-group">
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
             <div className="btn-chevron-menu-group">
               <div className="btn-group-chevron">
                 <div className="btn-group">
 
                   <button className={btnClassDecline}
                           onClick={this._handleDecline}>
-                    {__("incoming_call_cancel_button")}
+                    {mozL10n.get("incoming_call_cancel_button")}
                   </button>
                   <div className="btn-chevron"
                        onClick={this._toggleDeclineMenu}>
                   </div>
                 </div>
 
                 <ul className={dropdownMenuClassesDecline}>
                   <li className="btn-block" onClick={this._handleDeclineBlock}>
-                    {__("incoming_call_cancel_and_block_button")}
+                    {mozL10n.get("incoming_call_cancel_and_block_button")}
                   </li>
                 </ul>
 
               </div>
             </div>
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
-            <div className="btn-chevron-menu-group">
-              <div className="btn-group">
-                <button className={btnClassAccept}
-                        onClick={this._handleAccept("audio-video")}>
-                  <span className="fx-embedded-answer-btn-text">
-                    {__("incoming_call_accept_button")}
-                  </span>
-                  <span className="fx-embedded-btn-icon-video">
-                  </span>
-                </button>
-                <div className="call-audio-only"
-                     onClick={this._handleAccept("audio")}
-                     title={__("incoming_call_accept_audio_only_tooltip")} >
-                </div>
-              </div>
-            </div>
+            <AcceptCallButton mode={this._answerModeProps()} />
 
             <div className="fx-embedded-incoming-call-button-spacer"></div>
 
           </div>
         </div>
       );
       /* jshint ignore:end */
     }
   });
 
   /**
+   * Incoming call view accept button, renders different primary actions
+   * (answer with video / with audio only) based on the props received
+   **/
+  var AcceptCallButton = React.createClass({
+
+    propTypes: {
+      mode: React.PropTypes.object.isRequired,
+    },
+
+    render: function() {
+      var mode = this.props.mode;
+      return (
+        /* jshint ignore:start */
+        <div className="btn-chevron-menu-group">
+          <div className="btn-group">
+            <button className="btn btn-accept"
+                    onClick={mode.primary.handler}
+                    title={mozL10n.get(mode.primary.tooltip)}>
+              <span className="fx-embedded-answer-btn-text">
+                {mozL10n.get("incoming_call_accept_button")}
+              </span>
+              <span className={mode.primary.className}></span>
+            </button>
+            <div className={mode.secondary.className}
+                 onClick={mode.secondary.handler}
+                 title={mozL10n.get(mode.secondary.tooltip)}>
+            </div>
+          </div>
+        </div>
+        /* jshint ignore:end */
+      );
+    }
+  });
+
+  /**
    * Conversation router.
    *
    * Required options:
    * - {loop.shared.models.ConversationModel} conversation Conversation model.
    * - {loop.shared.models.NotificationCollection} notifications
    *
    * @type {loop.shared.router.BaseConversationRouter}
    */
@@ -220,17 +273,17 @@ loop.conversation = (function(OT, mozL10
       this._websocket = new loop.CallConnectionWebSocket({
         url: this._conversation.get("progressURL"),
         websocketToken: this._conversation.get("websocketToken"),
         callId: this._conversation.get("callId"),
       });
       this._websocket.promiseConnect().then(function() {
         this.loadReactComponent(loop.conversation.IncomingCallView({
           model: this._conversation,
-          video: {enabled: this._conversation.hasVideoStream("incoming")}
+          video: this._conversation.hasVideoStream("incoming")
         }));
       }.bind(this), function() {
         this._handleSessionError();
         return;
       }.bind(this));
     },
 
     /**
--- a/browser/components/loop/content/shared/css/common.css
+++ b/browser/components/loop/content/shared/css/common.css
@@ -220,37 +220,32 @@ p {
   border-radius: 2px;
   border-bottom-right-radius: 0;
   border-top-right-radius: 0;
 }
 
 /* Alerts */
 .alert {
   background: #eee;
-  padding: .2em 1em;
+  padding: .4em 1em;
   margin-bottom: 1em;
   border-bottom: 2px solid #E9E9E9;
 }
 
 .alert p.message {
   padding: 0;
   margin: 0;
 }
 
-.alert.alert-error {
-  display: flex;
-  align-content: center;
-  padding: 5px;
-  font-size: 10px;
-  justify-content: center;
-  color: #FFF;
+.alert-error {
   background: repeating-linear-gradient(-45deg, #D74345, #D74345 10px, #D94B4D 10px, #D94B4D 20px) repeat scroll 0% 0% transparent;
+  color: #fff;
 }
 
-.alert.alert-warning {
+.alert-warning {
   background: #fcf8e3;
   border: 1px solid #fbeed5;
 }
 
 .alert .close {
   position: relative;
   top: -.1rem;
   right: -1rem;
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -78,26 +78,64 @@
   }
 
 .fx-embedded-answer-btn-text {
   vertical-align: bottom;
   /* don't stretch the button if the localized text is too big */
   max-width: 80%;
 }
 
-.fx-embedded-btn-icon-video {
+.fx-embedded-btn-icon-video,
+.fx-embedded-btn-icon-audio {
   display: inline-block;
   vertical-align: top;
   width: .8rem;
   height: .8rem;
-  background-image: url("../img/video-inverse-14x14.png");
   background-repeat: no-repeat;
   cursor: pointer;
 }
 
+.fx-embedded-btn-icon-video,
+.fx-embedded-btn-video-small {
+  background-image: url("../img/video-inverse-14x14.png");
+}
+
+.fx-embedded-btn-icon-audio,
+.fx-embedded-btn-audio-small {
+  background-image: url("../img/audio-inverse-14x14.png");
+}
+
+.fx-embedded-btn-audio-small,
+.fx-embedded-btn-video-small {
+  width: 26px;
+  height: 26px;
+  border-left: 1px solid rgba(255,255,255,.4);
+  border-top-right-radius: 2px;
+  border-bottom-right-radius: 2px;
+  background-color: #74BF43;
+  background-position: center;
+  background-size: 1rem;
+  background-repeat: no-repeat;
+  cursor: pointer;
+}
+
+  .fx-embedded-btn-video-small:hover,
+  .fx-embedded-btn-audio-small:hover {
+    background-color: #6cb23e;
+  }
+
+@media (min-resolution: 2dppx) {
+  .fx-embedded-btn-audio-small {
+    background-image: url("../img/audio-inverse-14x14@2x.png");
+  }
+  .fx-embedded-btn-video-small {
+    background-image: url("../img/video-inverse-14x14@2x.png");
+  }
+}
+
 .standalone .btn-hangup {
   width: auto;
   font-size: 12px;
   border-radius: 2px;
   padding: 0 20px;
 }
 
 .fx-embedded .conversation-toolbar .btn-hangup {
@@ -228,40 +266,16 @@
   margin: 0.83em 0;
 }
 
 .fx-embedded-incoming-call-button-spacer {
   display: flex;
   flex: 1;
 }
 
-.call-audio-only {
-  width: 26px;
-  height: 26px;
-  border-left: 1px solid rgba(255,255,255,.4);
-  border-top-right-radius: 2px;
-  border-bottom-right-radius: 2px;
-  background-color: #74BF43;
-  background-image: url("../img/audio-inverse-14x14.png");
-  background-size: 1rem;
-  background-position: center;
-  background-repeat: no-repeat;
-  cursor: pointer;
-}
-
-  .call-audio-only:hover {
-    background-color: #6cb23e;
-  }
-
-@media (min-resolution: 2dppx) {
-  .call-audio-only {
-    background-image: url("../img/audio-inverse-14x14@2x.png");
-  }
-}
-
 /* Expired call url page */
 
 .expired-url-info {
   width: 400px;
   margin: 0 auto;
 }
 
 .promote-firefox {
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -237,27 +237,27 @@ describe("loop.conversation", function()
                   url: "http://progress.example.com",
                   // The websocket token is converted to a hex string.
                   websocketToken: "7b"
                 });
                 done();
               });
             });
 
-            it("should create the view with video.enabled=false", function(done) {
+            it("should create the view with video=false", function(done) {
               sandbox.stub(conversation, "get").withArgs("callType").returns("audio");
 
               router._setupWebSocketAndCallView();
 
               promise.then(function () {
                 sinon.assert.called(conversation.get);
                 sinon.assert.calledOnce(loop.conversation.IncomingCallView);
                 sinon.assert.calledWithExactly(loop.conversation.IncomingCallView,
                                                {model: conversation,
-                                               video: {enabled: false}});
+                                               video: false});
                 done();
               });
             });
           });
 
           describe("Websocket connection failed", function() {
             var promise;
 
@@ -579,66 +579,135 @@ describe("loop.conversation", function()
 
   describe("IncomingCallView", function() {
     var view, model;
 
     beforeEach(function() {
       var Model = Backbone.Model.extend({});
       model = new Model();
       sandbox.spy(model, "trigger");
+      sandbox.stub(model, "set");
+
       view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
-        model: model
+        model: model,
+        video: true
       }));
     });
 
+    describe("default answer mode", function() {
+      it("should display video as primary answer mode", function() {
+        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
+          model: model,
+          video: true
+        }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-video');
+
+        expect(primaryBtn).not.to.eql(null);
+      });
+
+      it("should display audio as primary answer mode", function() {
+        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
+          model: model,
+          video: false
+        }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-audio');
+
+        expect(primaryBtn).not.to.eql(null);
+      });
+
+      it("should accept call with video", function() {
+        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
+          model: model,
+          video: true
+        }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-video');
+
+        React.addons.TestUtils.Simulate.click(primaryBtn);
+
+        sinon.assert.calledOnce(model.set);
+        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
+        sinon.assert.calledOnce(model.trigger);
+        sinon.assert.calledWithExactly(model.trigger, "accept");
+      });
+
+      it("should accept call with audio", function() {
+        view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
+          model: model,
+          video: false
+        }));
+        var primaryBtn = view.getDOMNode()
+                                  .querySelector('.fx-embedded-btn-icon-audio');
+
+        React.addons.TestUtils.Simulate.click(primaryBtn);
+
+        sinon.assert.calledOnce(model.set);
+        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
+        sinon.assert.calledOnce(model.trigger);
+        sinon.assert.calledWithExactly(model.trigger, "accept");
+      });
+
+      it("should accept call with video when clicking on secondary btn",
+         function() {
+           view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
+             model: model,
+             video: false
+           }));
+           var secondaryBtn = view.getDOMNode()
+           .querySelector('.fx-embedded-btn-video-small');
+
+           React.addons.TestUtils.Simulate.click(secondaryBtn);
+
+           sinon.assert.calledOnce(model.set);
+           sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio-video");
+           sinon.assert.calledOnce(model.trigger);
+           sinon.assert.calledWithExactly(model.trigger, "accept");
+         });
+
+      it("should accept call with audio when clicking on secondary btn",
+         function() {
+           view = TestUtils.renderIntoDocument(loop.conversation.IncomingCallView({
+             model: model,
+             video: true
+           }));
+           var secondaryBtn = view.getDOMNode()
+           .querySelector('.fx-embedded-btn-audio-small');
+
+           React.addons.TestUtils.Simulate.click(secondaryBtn);
+
+           sinon.assert.calledOnce(model.set);
+           sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
+           sinon.assert.calledOnce(model.trigger);
+           sinon.assert.calledWithExactly(model.trigger, "accept");
+         });
+    });
+
     describe("click event on .btn-accept", function() {
       it("should trigger an 'accept' conversation model event", function () {
         var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
         model.trigger.withArgs("accept");
         TestUtils.Simulate.click(buttonAccept);
 
         /* Setting a model property triggers 2 events */
         sinon.assert.calledOnce(model.trigger.withArgs("accept"));
       });
 
       it("should set selectedCallType to audio-video", function () {
         var buttonAccept = view.getDOMNode().querySelector(".btn-accept");
-        sandbox.stub(model, "set");
 
         TestUtils.Simulate.click(buttonAccept);
 
         sinon.assert.calledOnce(model.set);
         sinon.assert.calledWithExactly(model.set, "selectedCallType",
           "audio-video");
       });
     });
 
-    describe("click event on .call-audio-only", function() {
-
-      it("should trigger an 'accept' conversation model event", function () {
-        var buttonAccept = view.getDOMNode().querySelector(".call-audio-only");
-        model.trigger.withArgs("accept");
-        TestUtils.Simulate.click(buttonAccept);
-
-        /* Setting a model property triggers 2 events */
-        sinon.assert.calledOnce(model.trigger.withArgs("accept"));
-      });
-
-
-      it("should set selectedCallType to audio", function() {
-        var buttonAccept = view.getDOMNode().querySelector(".call-audio-only");
-        sandbox.stub(model, "set");
-
-        TestUtils.Simulate.click(buttonAccept);
-
-        sinon.assert.calledOnce(model.set);
-        sinon.assert.calledWithExactly(model.set, "selectedCallType", "audio");
-      });
-    });
-
     describe("click event on .btn-decline", function() {
       it("should trigger an 'decline' conversation model event", function() {
         var buttonDecline = view.getDOMNode().querySelector(".btn-decline");
 
         TestUtils.Simulate.click(buttonDecline);
 
         sinon.assert.calledOnce(model.trigger);
         sinon.assert.calledWith(model.trigger, "decline");
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -130,27 +130,37 @@
               PanelView({client: mockClient, notifications: notifications})
             ), 
             Example({summary: "Error Notification", dashed: "true", style: {width: "332px"}}, 
               PanelView({client: mockClient, notifications: errNotifications})
             )
           ), 
 
           Section({name: "IncomingCallView"}, 
-            Example({summary: "Default", dashed: "true", style: {width: "280px"}}, 
+            Example({summary: "Default / incoming video call", dashed: "true", style: {width: "280px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
-                IncomingCallView({model: mockConversationModel})
+                IncomingCallView({model: mockConversationModel, 
+                                  video: {enabled: true}})
+              )
+            ), 
+
+            Example({summary: "Default / incoming audio only call", dashed: "true", style: {width: "280px"}}, 
+              React.DOM.div({className: "fx-embedded"}, 
+                IncomingCallView({model: mockConversationModel, 
+                                  video: {enabled: false}})
               )
             )
           ), 
 
           Section({name: "IncomingCallView-ActiveState"}, 
             Example({summary: "Default", dashed: "true", style: {width: "280px"}}, 
               React.DOM.div({className: "fx-embedded"}, 
-                IncomingCallView({model: mockConversationModel, showDeclineMenu: true})
+                IncomingCallView({model: mockConversationModel, 
+                                   showDeclineMenu: true, 
+                                   video: {enabled: true}})
               )
             )
           ), 
 
           Section({name: "ConversationToolbar"}, 
             React.DOM.h2(null, "Desktop Conversation Window"), 
             React.DOM.div({className: "fx-embedded override-position"}, 
               Example({summary: "Default (260x265)", dashed: "true"}, 
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -130,27 +130,37 @@
               <PanelView client={mockClient} notifications={notifications} />
             </Example>
             <Example summary="Error Notification" dashed="true" style={{width: "332px"}}>
               <PanelView client={mockClient} notifications={errNotifications}/>
             </Example>
           </Section>
 
           <Section name="IncomingCallView">
-            <Example summary="Default" dashed="true" style={{width: "280px"}}>
+            <Example summary="Default / incoming video call" dashed="true" style={{width: "280px"}}>
               <div className="fx-embedded">
-                <IncomingCallView model={mockConversationModel} />
+                <IncomingCallView model={mockConversationModel}
+                                  video={{enabled: true}} />
+              </div>
+            </Example>
+
+            <Example summary="Default / incoming audio only call" dashed="true" style={{width: "280px"}}>
+              <div className="fx-embedded">
+                <IncomingCallView model={mockConversationModel}
+                                  video={{enabled: false}} />
               </div>
             </Example>
           </Section>
 
           <Section name="IncomingCallView-ActiveState">
             <Example summary="Default" dashed="true" style={{width: "280px"}}>
               <div className="fx-embedded" >
-                <IncomingCallView  model={mockConversationModel} showDeclineMenu={true} />
+                <IncomingCallView  model={mockConversationModel}
+                                   showDeclineMenu={true}
+                                   video={{enabled: true}} />
               </div>
             </Example>
           </Section>
 
           <Section name="ConversationToolbar">
             <h2>Desktop Conversation Window</h2>
             <div className="fx-embedded override-position">
               <Example summary="Default (260x265)" dashed="true">
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -169,16 +169,17 @@ audio_call_menu_button=Audio Conversatio
 video_call_menu_button=Video Conversation
 
 # Conversation Window Strings
 
 initiate_call_button_label2=Ready to start your conversation?
 incoming_call_title2=Conversation Request
 incoming_call_accept_button=Accept
 incoming_call_accept_audio_only_tooltip=Accept with voice
+incoming_call_accept_audio_video_tooltip=Accept with video
 incoming_call_cancel_button=Cancel
 incoming_call_cancel_and_block_button=Cancel and Block
 incoming_call_block_button=Block
 hangup_button_title=Hang up
 hangup_button_caption2=Exit
 mute_local_audio_button_title=Mute your audio
 unmute_local_audio_button_title=Unmute your audio
 mute_local_video_button_title=Mute your video