Bug 987252 - Using new shared notification system. r=Standard8
authorNicolas Perriault <nperriault@gmail.com>
Thu, 29 May 2014 21:13:43 +0100
changeset 187667 c0f5493afd15712f41752339cdae24e37477a5cd
parent 187666 c0af40bf3f72389b61a1b1ce163f9bd050752e21
child 187668 1e682ccc611b0f62295b79c412036882be8112e7
push id26931
push usermbanner@mozilla.com
push dateMon, 09 Jun 2014 22:07:01 +0000
treeherdermozilla-central@fc70d6d9a9b0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersStandard8
bugs987252
milestone32.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 987252 - Using new shared notification system. r=Standard8
browser/components/loop/content/conversation.html
browser/components/loop/content/js/conversation.js
browser/components/loop/content/js/panel.js
browser/components/loop/content/panel.html
browser/components/loop/test/desktop-local/conversation_test.js
browser/components/loop/test/desktop-local/panel_test.js
browser/locales/en-US/chrome/browser/loop/loop.properties
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -5,26 +5,31 @@
 <html>
   <head>
     <meta charset="utf-8">
     <title>Conversation</title>
     <link rel="stylesheet" type="text/css" href="shared/css/common.css">
     <link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
  </head>
   <body onload="loop.conversation.init();">
+
+    <div id="messages"></div>
+
     <div id="conversation" class="conversation">
       <div class="media nested">
         <div class="remote">
           <div id="incoming"></div>
         </div>
         <div class="local">
           <div id="outgoing"></div>
         </div>
       </div>
     </div>
+
+    <script type="text/javascript" src="libs/l10n.js"></script>
     <script type="text/javascript" src="shared/libs/sdk.js"></script>
     <script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
     <script type="text/javascript" src="shared/js/client.js"></script>
     <script type="text/javascript" src="shared/js/models.js"></script>
     <script type="text/javascript" src="shared/js/router.js"></script>
     <script type="text/javascript" src="shared/js/views.js"></script>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -1,37 +1,47 @@
 /* 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/. */
 
-/*global loop*/
+/* global loop:true */
 
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 var loop = loop || {};
-loop.conversation = (function(TB) {
+loop.conversation = (function(TB, mozl10n) {
   "use strict";
 
-  var baseServerUrl = Services.prefs.getCharPref("loop.server");
+  var baseServerUrl = Services.prefs.getCharPref("loop.server"),
+      // aliasing translation function as __ for concision
+      __ = mozl10n.get;
 
   /**
    * App router.
    * @type {loop.webapp.Router}
    */
   var router;
 
   /**
    * Current conversation model instance.
    * @type {loop.webapp.ConversationModel}
    */
   var conversation;
 
+  /**
+   * Conversation router.
+   *
+   * Required options:
+   * - {loop.shared.models.ConversationModel} conversation Conversation model.
+   * - {loop.shared.components.Notifier}      notifier     Notifier component.
+   */
   var ConversationRouter = loop.shared.router.BaseRouter.extend({
     _conversation: undefined,
-    activeView: undefined,
+    _notifier:     undefined,
+    activeView:    undefined,
 
     routes: {
       "start/:version": "start",
       "call/ongoing": "conversation",
       "call/ended": "ended"
     },
 
     /**
@@ -48,16 +58,21 @@ loop.conversation = (function(TB) {
 
     initialize: function(options) {
       options = options || {};
       if (!options.conversation) {
         throw new Error("missing required conversation");
       }
       this._conversation = options.conversation;
 
+      if (!options.notifier) {
+        throw new Error("missing required notifier");
+      }
+      this._notifier = options.notifier;
+
       this.listenTo(this._conversation, "session:ready", this._onSessionReady);
       this.listenTo(this._conversation, "session:ended", this._onSessionEnded);
     },
 
     /**
      * Navigates to conversation when the call session is ready.
      */
     _onSessionReady: function() {
@@ -92,16 +107,17 @@ loop.conversation = (function(TB) {
      * conversation is the route when the conversation is active. The start
      * route should be navigated to first.
      */
     conversation: function() {
       if (!this._conversation.isSessionReady()) {
         // XXX: notify user that something has gone wrong.
         console.error("Error: navigated to conversation route without " +
           "the start route to initialise the call first");
+        this._notifier.notify(__("cannot_start_call_session_not_ready"));
         return;
       }
 
       this.loadView(
         new loop.shared.views.ConversationView({
           sdk: TB,
           model: this._conversation
       }));
@@ -116,17 +132,22 @@ loop.conversation = (function(TB) {
     }
   });
 
   /**
    * Panel initialisation.
    */
   function init() {
     conversation = new loop.shared.models.ConversationModel();
-    router = new ConversationRouter({conversation: conversation});
+    router = new ConversationRouter({
+      conversation: conversation,
+      notifier: new loop.shared.views.NotificationListView({
+        el: "#messages"
+      })
+    });
     Backbone.history.start();
   }
 
   return {
     ConversationRouter: ConversationRouter,
     init: init
   };
-})(window.TB);
+})(window.TB, document.mozL10n);
--- a/browser/components/loop/content/js/panel.js
+++ b/browser/components/loop/content/js/panel.js
@@ -1,165 +1,94 @@
 /* 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/. */
 
-/*global loop*/
+/* global loop:true */
+
 Components.utils.import("resource://gre/modules/Services.jsm");
 
 var loop = loop || {};
-loop.panel = (function(_, __) {
+loop.panel = (function(TB, mozl10n) {
   "use strict";
 
   var baseServerUrl = Services.prefs.getCharPref("loop.server"),
-      panelView;
-
-  /**
-   * Panel initialisation.
-   */
-  function init() {
-    panelView = new PanelView();
-    panelView.render();
-  }
-
-  /**
-   * Notification model.
-   */
-  var NotificationModel = Backbone.Model.extend({
-    defaults: {
-      level: "info",
-      message: ""
-    }
-  });
-
-  /**
-   * Notification collection
-   */
-  var NotificationCollection = Backbone.Collection.extend({
-    model: NotificationModel
-  });
-
-  /**
-   * Notification view.
-   */
-  var NotificationView = Backbone.View.extend({
-    template: _.template([
-      '<div class="alert alert-<%- level %>">',
-      '  <button class="close"></button>',
-      '  <p class="message"><%- message %></p>',
-      '</div>'
-    ].join("")),
-
-    events: {
-      "click .close": "dismiss"
-    },
-
-    dismiss: function() {
-      this.$el.addClass("fade-out");
-      setTimeout(function() {
-        this.collection.remove(this.model);
-        this.remove();
-      }.bind(this), 500);
-    },
-
-    render: function() {
-      this.$el.html(this.template(this.model.toJSON()));
-      return this;
-    }
-  });
-
-  /**
-   * Notification list view.
-   */
-  var NotificationListView = Backbone.View.extend({
-    initialize: function() {
-      this.listenTo(this.collection, "reset add remove", this.render);
-    },
-
-    render: function() {
-      this.$el.html(this.collection.map(function(notification) {
-        return new NotificationView({
-          model: notification,
-          collection: this.collection
-        }).render().$el;
-      }.bind(this)));
-      return this;
-    }
-  });
+      panelView,
+      // aliasing translation function as __ for concision
+      __ = mozl10n.get;
 
   /**
    * Panel view.
+   *
+   * XXX view layout changes should be handled by a router really.
    */
   var PanelView = Backbone.View.extend({
     el: "#default-view",
 
     events: {
       "keyup input[name=caller]": "changeButtonState",
       "click a.get-url": "getCallUrl",
       "click a.go-back": "goBack"
     },
 
     initialize: function() {
       this.client = new loop.shared.Client({
         baseServerUrl: baseServerUrl
       });
-      this.notificationCollection = new NotificationCollection();
-      this.notificationListView = new NotificationListView({
-        el: this.$(".messages"),
-        collection: this.notificationCollection
-      });
-      this.notificationListView.render();
-    },
-
-    notify: function(message, level) {
-      this.notificationCollection.add({
-        level: level || "info",
-        message: message
-      });
+      this.notifier = new loop.shared.views.NotificationListView({
+        el: this.$(".messages")
+      }).render();
     },
 
     getCallUrl: function(event) {
       event.preventDefault();
       var nickname = this.$("input[name=caller]").val();
       var callback = function(err, callUrl) {
         if (err) {
-          this.notify(__("unable_retrieve_url"), "error");
+          this.notifier.notify({
+            message: __("unable_retrieve_url"),
+            level: "error"
+          });
           return;
         }
         this.onCallUrlReceived(callUrl);
       }.bind(this);
 
       this.client.requestCallUrl(nickname, callback);
     },
 
     goBack: function(event) {
       this.$(".action .result").hide();
       this.$(".action .invite").show();
       this.$(".description p").text(__("get_link_to_share"));
     },
 
     onCallUrlReceived: function(callUrl) {
-      this.notificationCollection.reset();
+      this.notifier.clear();
       this.$(".action .invite").hide();
       this.$(".action .invite input").val("");
       this.$(".action .result input").val(callUrl);
       this.$(".action .result").show();
       this.$(".description p").text(__("share_link_url"));
     },
 
     changeButtonState: function() {
       var enabled = !!this.$("input[name=caller]").val();
       if (enabled)
         this.$("a.get-url").removeClass("disabled");
       else
         this.$("a.get-url").addClass("disabled");
     }
   });
 
+  /**
+   * Panel initialisation.
+   */
+  function init() {
+    panelView = new PanelView();
+    panelView.render();
+  }
+
   return {
     init: init,
-    NotificationModel: NotificationModel,
-    NotificationCollection: NotificationCollection,
-    NotificationView: NotificationView,
-    NotificationListView: NotificationListView,
     PanelView: PanelView
   };
-})(_, document.mozL10n.get);
+})(_, document.mozL10n);
--- a/browser/components/loop/content/panel.html
+++ b/browser/components/loop/content/panel.html
@@ -40,11 +40,13 @@
          library as pdf.js, we should be referring to the same
          file. -->
     <script type="text/javascript" src="js/fxcom.js"></script>
     <script type="text/javascript" src="libs/l10n.js"></script>
     <script type="text/javascript" src="shared/libs/jquery-2.1.0.js"></script>
     <script type="text/javascript" src="shared/libs/lodash-2.4.1.js"></script>
     <script type="text/javascript" src="shared/libs/backbone-1.1.2.js"></script>
     <script type="text/javascript" src="shared/js/client.js"></script>
+    <script type="text/javascript" src="shared/js/models.js"></script>
+    <script type="text/javascript" src="shared/js/views.js"></script>
     <script type="text/javascript" src="js/panel.js"></script>
   </body>
 </html>
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -5,20 +5,22 @@
 /* global loop, sinon */
 
 var expect = chai.expect;
 
 describe("loop.conversation", function() {
   "use strict";
 
   var ConversationRouter = loop.conversation.ConversationRouter,
-      sandbox;
+      sandbox,
+      notifier;
 
   beforeEach(function() {
     sandbox = sinon.sandbox.create();
+    notifier = {notify: sandbox.spy()};
   });
 
   afterEach(function() {
     sandbox.restore();
   });
 
   describe("ConversationRouter", function() {
     var conversation;
@@ -28,23 +30,32 @@ describe("loop.conversation", function()
     });
 
     describe("#constructor", function() {
       it("should require a ConversationModel instance", function() {
         expect(function() {
           new ConversationRouter();
         }).to.Throw(Error, /missing required conversation/);
       });
+
+      it("should require a notifier", function() {
+        expect(function() {
+          new ConversationRouter({conversation: conversation});
+        }).to.Throw(Error, /missing required notifier/);
+      });
     });
 
     describe("Routes", function() {
       var router;
 
       beforeEach(function() {
-        router = new ConversationRouter({conversation: conversation});
+        router = new ConversationRouter({
+          conversation: conversation,
+          notifier: notifier
+        });
         sandbox.stub(router, "loadView");
       });
 
       describe("#start", function() {
         it("should set the loopVersion on the conversation model", function() {
           router.start("fakeVersion");
 
           expect(conversation.get("loopVersion")).to.equal("fakeVersion");
@@ -78,16 +89,23 @@ describe("loop.conversation", function()
         });
 
         it("should not load the ConversationView if session is not set",
           function() {
             router.conversation();
 
             sinon.assert.notCalled(router.loadView);
         });
+
+        it("should notify the user when session is not set",
+          function() {
+            router.conversation();
+
+            sinon.assert.calledOnce(router._notifier.notify);
+        });
       });
 
       describe("#ended", function() {
         // XXX When the call is ended gracefully, we should check that we
         // close connections nicely
         it("should close the window", function() {
           sandbox.stub(window, "close");
 
@@ -108,30 +126,32 @@ describe("loop.conversation", function()
           apiKey:       "apiKey"
         };
       });
 
       it("should navigate to call/ongoing once the call session is ready",
         function() {
           sandbox.stub(ConversationRouter.prototype, "navigate");
           var router = new ConversationRouter({
-            conversation: conversation
+            conversation: conversation,
+            notifier: notifier
           });
 
           conversation.setReady(fakeSessionData);
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/ongoing");
         });
 
       it("should navigate to call/ended when the call session ends",
         function() {
           sandbox.stub(ConversationRouter.prototype, "navigate");
           var router = new ConversationRouter({
-            conversation: conversation
+            conversation: conversation,
+            notifier: notifier
           });
 
           conversation.trigger("session:ended");
 
           sinon.assert.calledOnce(router.navigate);
           sinon.assert.calledWith(router.navigate, "call/ended");
         });
     });
--- a/browser/components/loop/test/desktop-local/panel_test.js
+++ b/browser/components/loop/test/desktop-local/panel_test.js
@@ -21,71 +21,16 @@ describe("loop.panel", function() {
     };
   });
 
   afterEach(function() {
     $("#fixtures").empty();
     sandbox.restore();
   });
 
-  describe("loop.panel.NotificationView", function() {
-    describe("#render", function() {
-      it("should render template with model attribute values", function() {
-        var view = new loop.panel.NotificationView({
-          el: $("#fixtures"),
-          model: new loop.panel.NotificationModel({
-            level: "error",
-            message: "plop"
-          })
-        });
-
-        view.render();
-
-        expect(view.$(".message").text()).eql("plop");
-      });
-    });
-  });
-
-  describe("loop.panel.NotificationListView", function() {
-    describe("Collection events", function() {
-      var coll, testNotif, view;
-
-      beforeEach(function() {
-        sandbox.stub(loop.panel.NotificationListView.prototype, "render");
-        testNotif = new loop.panel.NotificationModel({
-          level: "error",
-          message: "plop"
-        });
-        coll = new loop.panel.NotificationCollection();
-        view = new loop.panel.NotificationListView({collection: coll});
-      });
-
-      it("should render on notification added to the collection", function() {
-        coll.add(testNotif);
-
-        sinon.assert.calledOnce(view.render);
-      });
-
-      it("should render on notification removed from the collection",
-        function() {
-          coll.add(testNotif);
-          coll.remove(testNotif);
-
-          sinon.assert.calledTwice(view.render);
-        });
-
-      it("should render on collection reset",
-        function() {
-          coll.reset();
-
-          sinon.assert.calledOnce(view.render);
-        });
-    });
-  });
-
   describe("loop.panel.PanelView", function() {
     beforeEach(function() {
       $("#fixtures").append([
         '<div id="default-view" class="share generate-url">',
         '  <div class="description">',
         '    <p>Get a link to share with a friend to Video Chat.</p>',
         '  </div>',
         '  <div class="action">',
@@ -99,33 +44,56 @@ describe("loop.panel", function() {
         '      <input id="call-url" type="url" readonly>',
         '      <a class="get-url" href="">Renew</a>',
         '    </p>',
         '  </div>',
         '</div>'
       ].join(""));
     });
 
-    describe("#getCallurl", function() {
+    describe("#getCallUrl", function() {
       it("should request a call url to the server", function() {
         var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
                                           "requestCallUrl");
         var view = new loop.panel.PanelView();
 
         view.getCallUrl({preventDefault: sandbox.spy()});
 
         sinon.assert.calledOnce(requestCallUrl);
         sinon.assert.calledWith(requestCallUrl, "foo");
       });
+
+      it("should notify the user when the operation failed", function() {
+        var requestCallUrl = sandbox.stub(
+          loop.shared.Client.prototype, "requestCallUrl", function(_, cb) {
+            cb("fake error");
+          });
+        var view = new loop.panel.PanelView();
+        sandbox.stub(view.notifier, "notify");
+
+        view.getCallUrl({preventDefault: sandbox.spy()});
+
+        sinon.assert.calledOnce(view.notifier.notify);
+        sinon.assert.calledWithMatch(view.notifier.notify, {level: "error"});
+      });
     });
 
     describe("#onCallUrlReceived", function() {
       it("should update the text field with the call url", function() {
         var view = new loop.panel.PanelView();
         view.render();
 
         view.onCallUrlReceived("http://call.me/");
 
         expect(view.$("#call-url").val()).eql("http://call.me/");
       });
+
+      it("should reset all pending notifications", function() {
+        var view = new loop.panel.PanelView().render();
+        sandbox.stub(view.notifier, "clear");
+
+        view.onCallUrlReceived("http://call.me/");
+
+        sinon.assert.calledOnce(view.notifier.clear, "clear");
+      });
     });
   });
 });
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -1,6 +1,7 @@
 get_link_to_share=Get a link to share with a friend to Video Chat.
 get_a_call_url=Get a call url
 new_url=New url
 unable_retrieve_url=Sorry, we were unable to retrieve a call url.
 share_link_url=Share the link below with your friend to start your call!
 caller.placeholder=Identify this call
+cannot_start_call_session_not_ready=Can't start call, session is not ready.