Bug 987252 - Using new shared notification system. r=Standard8
authorNicolas Perriault <nperriault@gmail.com>
Thu, 29 May 2014 21:13:43 +0100
changeset 187717 c0f5493afd15712f41752339cdae24e37477a5cd
parent 187716 c0af40bf3f72389b61a1b1ce163f9bd050752e21
child 187718 1e682ccc611b0f62295b79c412036882be8112e7
push idunknown
push userunknown
push dateunknown
reviewersStandard8
bugs987252
milestone32.0a1
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.