Bug 1171949 - Play a sound when a chat message is received for Loop. r=dmose
authorMark Banner <standard8@mozilla.com>
Wed, 24 Jun 2015 13:36:48 -0700
changeset 268217 edda2b44b087238cadd4af6908c71b4aa38dfc26
parent 268216 832cef9745600af087abde7492daac9817f2117a
child 268218 c843008bfe6f226b35ce831dccd0d06b6ec95cca
push id4932
push userjlund@mozilla.com
push dateMon, 10 Aug 2015 18:23:06 +0000
treeherdermozilla-esr52@6dd5a4f5f745 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersdmose
bugs1171949
milestone41.0a1
Bug 1171949 - Play a sound when a chat message is received for Loop. r=dmose
browser/components/loop/content/js/roomViews.js
browser/components/loop/content/js/roomViews.jsx
browser/components/loop/content/shared/js/mixins.js
browser/components/loop/content/shared/js/textChatView.js
browser/components/loop/content/shared/js/textChatView.jsx
browser/components/loop/content/shared/sounds/message.ogg
browser/components/loop/jar.mn
browser/components/loop/standalone/content/js/standaloneRoomViews.js
browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
browser/components/loop/test/shared/textChatView_test.js
browser/components/loop/ui/ui-showcase.js
browser/components/loop/ui/ui-showcase.jsx
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -799,17 +799,17 @@ loop.roomViews = (function(mozL10n) {
               ), 
               React.createElement(DesktopRoomContextView, {
                 dispatcher: this.props.dispatcher, 
                 error: this.state.error, 
                 mozLoop: this.props.mozLoop, 
                 roomData: roomData, 
                 savingContext: this.state.savingContext, 
                 show: !shouldRenderInvitationOverlay && shouldRenderContextView}), 
-              React.createElement(sharedViews.TextChatView, {
+              React.createElement(sharedViews.chat.TextChatView, {
                 dispatcher: this.props.dispatcher, 
                 showAlways: false, 
                 showRoomName: false})
             )
           );
         }
       }
     }
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -799,17 +799,17 @@ loop.roomViews = (function(mozL10n) {
               </div>
               <DesktopRoomContextView
                 dispatcher={this.props.dispatcher}
                 error={this.state.error}
                 mozLoop={this.props.mozLoop}
                 roomData={roomData}
                 savingContext={this.state.savingContext}
                 show={!shouldRenderInvitationOverlay && shouldRenderContextView} />
-              <sharedViews.TextChatView
+              <sharedViews.chat.TextChatView
                 dispatcher={this.props.dispatcher}
                 showAlways={false}
                 showRoomName={false} />
             </div>
           );
         }
       }
     }
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -600,17 +600,19 @@ loop.shared.mixins = (function() {
     _isLoopDesktop: function() {
       return rootObject.navigator &&
              typeof rootObject.navigator.mozLoop === "object";
     },
 
     /**
      * Starts playing an audio file, stopping any audio that is already in progress.
      *
-     * @param {String} name The filename to play (excluding the extension).
+     * @param {String} name    The filename to play (excluding the extension).
+     * @param {Object} options A list of options for the sound:
+     *                         - {Boolean} loop Whether or not to loop the sound.
      */
     play: function(name, options) {
       if (this._isLoopDesktop() && rootObject.navigator.mozLoop.doNotDisturb) {
         return;
       }
 
       options = options || {};
       options.loop = options.loop || false;
--- a/browser/components/loop/content/shared/js/textChatView.js
+++ b/browser/components/loop/content/shared/js/textChatView.js
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = loop.shared.views || {};
-loop.shared.views.TextChatView = (function(mozL10n) {
+loop.shared.views.chat = (function(mozL10n) {
   var sharedActions = loop.shared.actions;
+  var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
 
   /**
    * Renders an individual entry for the text chat entries view.
    */
   var TextChatEntry = React.createClass({displayName: "TextChatEntry",
@@ -56,32 +57,53 @@ loop.shared.views.TextChatView = (functi
   });
 
   /**
    * Manages the text entries in the chat entries view. This is split out from
    * TextChatView so that scrolling can be managed more efficiently - this
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({displayName: "TextChatEntriesView",
-    mixins: [React.addons.PureRenderMixin],
+    mixins: [
+      React.addons.PureRenderMixin,
+      sharedMixins.AudioMixin
+    ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       messageList: React.PropTypes.array.isRequired
     },
 
+    getInitialState: function() {
+      return {
+        receivedMessageCount: 0
+      };
+    },
+
     componentWillUpdate: function() {
       var node = this.getDOMNode();
       if (!node) {
         return;
       }
       // Scroll only if we're right at the bottom of the display.
       this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
     },
 
+    componentWillReceiveProps: function(nextProps) {
+      var receivedMessageCount = nextProps.messageList.filter(function(message) {
+        return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
+      }).length;
+
+      // If the number of received messages has increased, we play a sound.
+      if (receivedMessageCount > this.state.receivedMessageCount) {
+        this.play("message");
+        this.setState({receivedMessageCount: receivedMessageCount});
+      }
+    },
+
     componentDidUpdate: function() {
       if (this.shouldScroll) {
         // This ensures the paint is complete.
         window.requestAnimationFrame(function() {
           try {
             var node = this.getDOMNode();
             node.scrollTop = node.scrollHeight - node.clientHeight;
           } catch (ex) {
@@ -280,10 +302,13 @@ loop.shared.views.TextChatView = (functi
             dispatcher: this.props.dispatcher, 
             showPlaceholder: !hasNonSpecialMessages, 
             textChatEnabled: this.state.textChatEnabled})
         )
       );
     }
   });
 
-  return TextChatView;
+  return {
+    TextChatEntriesView: TextChatEntriesView,
+    TextChatView: TextChatView
+  };
 })(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/textChatView.jsx
+++ b/browser/components/loop/content/shared/js/textChatView.jsx
@@ -1,17 +1,18 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this file,
  * You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 var loop = loop || {};
 loop.shared = loop.shared || {};
 loop.shared.views = loop.shared.views || {};
-loop.shared.views.TextChatView = (function(mozL10n) {
+loop.shared.views.chat = (function(mozL10n) {
   var sharedActions = loop.shared.actions;
+  var sharedMixins = loop.shared.mixins;
   var sharedViews = loop.shared.views;
   var CHAT_MESSAGE_TYPES = loop.store.CHAT_MESSAGE_TYPES;
   var CHAT_CONTENT_TYPES = loop.store.CHAT_CONTENT_TYPES;
 
   /**
    * Renders an individual entry for the text chat entries view.
    */
   var TextChatEntry = React.createClass({
@@ -56,32 +57,53 @@ loop.shared.views.TextChatView = (functi
   });
 
   /**
    * Manages the text entries in the chat entries view. This is split out from
    * TextChatView so that scrolling can be managed more efficiently - this
    * component only updates when the message list is changed.
    */
   var TextChatEntriesView = React.createClass({
-    mixins: [React.addons.PureRenderMixin],
+    mixins: [
+      React.addons.PureRenderMixin,
+      sharedMixins.AudioMixin
+    ],
 
     propTypes: {
       dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
       messageList: React.PropTypes.array.isRequired
     },
 
+    getInitialState: function() {
+      return {
+        receivedMessageCount: 0
+      };
+    },
+
     componentWillUpdate: function() {
       var node = this.getDOMNode();
       if (!node) {
         return;
       }
       // Scroll only if we're right at the bottom of the display.
       this.shouldScroll = node.scrollHeight === node.scrollTop + node.clientHeight;
     },
 
+    componentWillReceiveProps: function(nextProps) {
+      var receivedMessageCount = nextProps.messageList.filter(function(message) {
+        return message.type === CHAT_MESSAGE_TYPES.RECEIVED;
+      }).length;
+
+      // If the number of received messages has increased, we play a sound.
+      if (receivedMessageCount > this.state.receivedMessageCount) {
+        this.play("message");
+        this.setState({receivedMessageCount: receivedMessageCount});
+      }
+    },
+
     componentDidUpdate: function() {
       if (this.shouldScroll) {
         // This ensures the paint is complete.
         window.requestAnimationFrame(function() {
           try {
             var node = this.getDOMNode();
             node.scrollTop = node.scrollHeight - node.clientHeight;
           } catch (ex) {
@@ -280,10 +302,13 @@ loop.shared.views.TextChatView = (functi
             dispatcher={this.props.dispatcher}
             showPlaceholder={!hasNonSpecialMessages}
             textChatEnabled={this.state.textChatEnabled} />
         </div>
       );
     }
   });
 
-  return TextChatView;
+  return {
+    TextChatEntriesView: TextChatEntriesView,
+    TextChatView: TextChatView
+  };
 })(navigator.mozL10n || document.mozL10n);
new file mode 100644
index e69de29bb2d1d6434b8b29ae775ad8c2e48c5391..920b458254b5b346671441461120b0ecb7973c7b
GIT binary patch
literal 14063
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%0A0U%^k0fb2gw
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
ABLDyZ
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -107,16 +107,17 @@ browser.jar:
   content/browser/loop/shared/sounds/ringtone.ogg       (content/shared/sounds/ringtone.ogg)
   content/browser/loop/shared/sounds/connecting.ogg     (content/shared/sounds/connecting.ogg)
   content/browser/loop/shared/sounds/connected.ogg      (content/shared/sounds/connected.ogg)
   content/browser/loop/shared/sounds/terminated.ogg     (content/shared/sounds/terminated.ogg)
   content/browser/loop/shared/sounds/room-joined.ogg    (content/shared/sounds/room-joined.ogg)
   content/browser/loop/shared/sounds/room-joined-in.ogg (content/shared/sounds/room-joined-in.ogg)
   content/browser/loop/shared/sounds/room-left.ogg      (content/shared/sounds/room-left.ogg)
   content/browser/loop/shared/sounds/failure.ogg        (content/shared/sounds/failure.ogg)
+  content/browser/loop/shared/sounds/message.ogg        (content/shared/sounds/message.ogg)
 
   # Partner SDK assets
   content/browser/loop/libs/sdk.js                                                    (content/shared/libs/sdk.js)
   content/browser/loop/sdk-content/css/ot.css                                 (content/shared/libs/sdk-content/css/ot.css)
   content/browser/loop/sdk-content/js/dynamic_config.min.js                   (content/shared/libs/sdk-content/js/dynamic_config.min.js)
   content/browser/loop/sdk-content/images/rtc/access-denied-chrome.png        (content/shared/libs/sdk-content/images/rtc/access-denied-chrome.png)
   content/browser/loop/sdk-content/images/rtc/access-denied-copy-firefox.png  (content/shared/libs/sdk-content/images/rtc/access-denied-copy-firefox.png)
   content/browser/loop/sdk-content/images/rtc/access-denied-firefox.png       (content/shared/libs/sdk-content/images/rtc/access-denied-firefox.png)
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -456,17 +456,17 @@ loop.standaloneRoomViews = (function(moz
               ), 
               React.createElement("div", {className: screenShareStreamClasses}, 
                 React.createElement(sharedViews.MediaView, {displayAvatar: false, 
                   isLoading: this._shouldRenderScreenShareLoading(), 
                   mediaType: "screen-share", 
                   posterUrl: this.props.screenSharePosterUrl, 
                   srcVideoObject: this.state.screenShareVideoObject})
               ), 
-              React.createElement(sharedViews.TextChatView, {
+              React.createElement(sharedViews.chat.TextChatView, {
                 dispatcher: this.props.dispatcher, 
                 showAlways: true, 
                 showRoomName: true}), 
               React.createElement("div", {className: "local"}, 
                 React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted, 
                   isLoading: this._shouldRenderLocalLoading(), 
                   mediaType: "local", 
                   posterUrl: this.props.localPosterUrl, 
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -456,17 +456,17 @@ loop.standaloneRoomViews = (function(moz
               </div>
               <div className={screenShareStreamClasses}>
                 <sharedViews.MediaView displayAvatar={false}
                   isLoading={this._shouldRenderScreenShareLoading()}
                   mediaType="screen-share"
                   posterUrl={this.props.screenSharePosterUrl}
                   srcVideoObject={this.state.screenShareVideoObject} />
               </div>
-              <sharedViews.TextChatView
+              <sharedViews.chat.TextChatView
                 dispatcher={this.props.dispatcher}
                 showAlways={true}
                 showRoomName={true} />
               <div className="local">
                 <sharedViews.MediaView displayAvatar={this.state.videoMuted}
                   isLoading={this._shouldRenderLocalLoading()}
                   mediaType="local"
                   posterUrl={this.props.localPosterUrl}
--- a/browser/components/loop/test/shared/textChatView_test.js
+++ b/browser/components/loop/test/shared/textChatView_test.js
@@ -32,25 +32,108 @@ describe("loop.shared.views.TextChatView
       textChatStore: store
     });
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
+  describe("TextChatEntriesView", function() {
+    var view;
+
+    function mountTestComponent(extraProps) {
+      var basicProps = {
+        dispatcher: dispatcher,
+        messageList: []
+      };
+
+      return TestUtils.renderIntoDocument(
+        React.createElement(loop.shared.views.chat.TextChatEntriesView,
+          _.extend(basicProps, extraProps)));
+    }
+
+    it("should render message entries when message were sent/ received", function() {
+      view = mountTestComponent({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!"
+        }, {
+          type: CHAT_MESSAGE_TYPES.SENT,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Is it me you're looking for?"
+        }]
+      });
+
+      var node = view.getDOMNode();
+      expect(node).to.not.eql(null);
+
+      var entries = node.querySelectorAll(".text-chat-entry");
+      expect(entries.length).to.eql(2);
+      expect(entries[0].classList.contains("received")).to.eql(true);
+      expect(entries[1].classList.contains("received")).to.not.eql(true);
+    });
+
+    it("should play a sound when a message is received", function() {
+      view = mountTestComponent();
+      sandbox.stub(view, "play");
+
+      view.setProps({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.RECEIVED,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!"
+        }]
+      });
+
+      sinon.assert.calledOnce(view.play);
+      sinon.assert.calledWithExactly(view.play, "message");
+    });
+
+    it("should not play a sound when a special message is displayed", function() {
+      view = mountTestComponent();
+      sandbox.stub(view, "play");
+
+      view.setProps({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.SPECIAL,
+          contentType: CHAT_CONTENT_TYPES.ROOM_NAME,
+          message: "Hello!"
+        }]
+      });
+
+      sinon.assert.notCalled(view.play);
+    });
+
+    it("should not play a sound when a message is sent", function() {
+      view = mountTestComponent();
+      sandbox.stub(view, "play");
+
+      view.setProps({
+        messageList: [{
+          type: CHAT_MESSAGE_TYPES.SENT,
+          contentType: CHAT_CONTENT_TYPES.TEXT,
+          message: "Hello!"
+        }]
+      });
+
+      sinon.assert.notCalled(view.play);
+    });
+  });
+
   describe("TextChatView", function() {
     var view;
 
     function mountTestComponent(extraProps) {
       var props = _.extend({
         dispatcher: dispatcher
       }, extraProps);
       return TestUtils.renderIntoDocument(
-        React.createElement(loop.shared.views.TextChatView, props));
+        React.createElement(loop.shared.views.chat.TextChatView, props));
     }
 
     beforeEach(function() {
       store.setStoreState({ textChatEnabled: true });
     });
 
     it("should not display anything if no messages and text chat not enabled and showAlways is false", function() {
       store.setStoreState({ textChatEnabled: false });
@@ -84,37 +167,16 @@ describe("loop.shared.views.TextChatView
       view = mountTestComponent();
 
       var node = view.getDOMNode();
 
       expect(node.querySelector(".text-chat-box")).not.eql(null);
       expect(node.querySelector(".text-chat-entries")).eql(null);
     });
 
-    it("should render message entries when message were sent/ received", function() {
-      view = mountTestComponent();
-
-      store.receivedTextChatMessage({
-        contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: "Hello!"
-      });
-      store.sendTextChatMessage({
-        contentType: CHAT_CONTENT_TYPES.TEXT,
-        message: "Is it me you're looking for?"
-      });
-
-      var node = view.getDOMNode();
-      expect(node.querySelector(".text-chat-entries")).to.not.eql(null);
-
-      var entries = node.querySelectorAll(".text-chat-entry");
-      expect(entries.length).to.eql(2);
-      expect(entries[0].classList.contains("received")).to.eql(true);
-      expect(entries[1].classList.contains("received")).to.not.eql(true);
-    });
-
     it("should render a room name special entry", function() {
       view = mountTestComponent({
         showRoomName: true
       });
 
       store.updateRoomInfo(new sharedActions.UpdateRoomInfo({
         roomName: "A wonderful surprise!",
         roomOwner: "Chris",
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -29,17 +29,17 @@
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var FeedbackView = loop.shared.views.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
-  var TextChatView = loop.shared.views.TextChatView;
+  var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -29,17 +29,17 @@
   var UnsupportedBrowserView  = loop.webapp.UnsupportedBrowserView;
   var UnsupportedDeviceView   = loop.webapp.UnsupportedDeviceView;
   var StandaloneRoomView      = loop.standaloneRoomViews.StandaloneRoomView;
 
   // 3. Shared components
   var ConversationToolbar = loop.shared.views.ConversationToolbar;
   var FeedbackView = loop.shared.views.FeedbackView;
   var Checkbox = loop.shared.views.Checkbox;
-  var TextChatView = loop.shared.views.TextChatView;
+  var TextChatView = loop.shared.views.chat.TextChatView;
 
   // Store constants
   var ROOM_STATES = loop.store.ROOM_STATES;
   var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
   var CALL_TYPES = loop.shared.utils.CALL_TYPES;
 
   // Local helpers
   function returnTrue() {