Bug 988229 - Shared notifier component. r=dmose
This commit is contained in:
@@ -9,6 +9,8 @@ loop.shared = loop.shared || {};
|
||||
loop.shared.views = (function(TB) {
|
||||
"use strict";
|
||||
|
||||
var sharedModels = loop.shared.models;
|
||||
|
||||
/**
|
||||
* Base Backbone view.
|
||||
*/
|
||||
@@ -127,10 +129,39 @@ loop.shared.views = (function(TB) {
|
||||
* Notification list view.
|
||||
*/
|
||||
var NotificationListView = Backbone.View.extend({
|
||||
initialize: function() {
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Available options:
|
||||
* - {loop.shared.models.NotificationCollection} collection Notifications
|
||||
* collection
|
||||
*
|
||||
* @param {Object} options Options object
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
if (!options.collection) {
|
||||
this.collection = new sharedModels.NotificationCollection();
|
||||
}
|
||||
this.listenTo(this.collection, "reset add remove", this.render);
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears the notification stack.
|
||||
*/
|
||||
clear: function() {
|
||||
this.collection.reset();
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds a new notification to the stack, triggering rendering of it.
|
||||
*
|
||||
* @param {Object|NotificationModel} notification Notification data.
|
||||
*/
|
||||
notify: function(notification) {
|
||||
this.collection.add(notification);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
this.$el.html(this.collection.map(function(notification) {
|
||||
return new NotificationView({
|
||||
|
||||
@@ -17,6 +17,8 @@
|
||||
<h1>Loop</h1>
|
||||
</header>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<div id="home">
|
||||
<p data-l10n-id="welcome">Welcome to the Loop web client.</p>
|
||||
</div>
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
/* global loop:true */
|
||||
|
||||
var loop = loop || {};
|
||||
loop.webapp = (function($, TB) {
|
||||
loop.webapp = (function($, TB, webl10n) {
|
||||
"use strict";
|
||||
|
||||
/**
|
||||
@@ -19,7 +19,9 @@ loop.webapp = (function($, TB) {
|
||||
sharedViews = loop.shared.views,
|
||||
// XXX this one should be configurable
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=987086
|
||||
baseServerUrl = "http://localhost:5000";
|
||||
baseServerUrl = "http://localhost:5000",
|
||||
// aliasing translation function as __ for concision
|
||||
__ = webl10n.get;
|
||||
|
||||
/**
|
||||
* App router.
|
||||
@@ -51,11 +53,36 @@ loop.webapp = (function($, TB) {
|
||||
"submit": "initiate"
|
||||
},
|
||||
|
||||
initialize: function() {
|
||||
this.listenTo(this.model, "session:error", function(error) {
|
||||
// XXX: display a proper error notification to end user, probably
|
||||
// reusing the BB notification system from the Loop desktop client.
|
||||
alert(error);
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
* Required options:
|
||||
* - {loop.shared.model.ConversationModel} model Conversation model.
|
||||
* - {loop.shared.views.NotificationListView} notifier Notifier component.
|
||||
*
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
initialize: function(options) {
|
||||
options = options || {};
|
||||
|
||||
if (!options.model) {
|
||||
throw new Error("missing required model");
|
||||
}
|
||||
this.model = options.model;
|
||||
|
||||
if (!options.notifier) {
|
||||
throw new Error("missing required notifier");
|
||||
}
|
||||
this.notifier = options.notifier;
|
||||
|
||||
this.listenTo(this.model, "session:error", this._onSessionError);
|
||||
},
|
||||
|
||||
_onSessionError: function(error) {
|
||||
console.error(error);
|
||||
this.notifier.notify({
|
||||
message: __("unable_retrieve_call_info"),
|
||||
level: "error"
|
||||
});
|
||||
},
|
||||
|
||||
@@ -75,8 +102,18 @@ loop.webapp = (function($, TB) {
|
||||
* @link http://mikeygee.com/blog/backbone.html
|
||||
*/
|
||||
var WebappRouter = loop.shared.router.BaseRouter.extend({
|
||||
/**
|
||||
* Current conversation.
|
||||
* @type {loop.shared.models.ConversationModel}
|
||||
*/
|
||||
_conversation: undefined,
|
||||
|
||||
/**
|
||||
* Notifications dispatcher.
|
||||
* @type {loop.shared.views.NotificationListView}
|
||||
*/
|
||||
_notifier: undefined,
|
||||
|
||||
routes: {
|
||||
"": "home",
|
||||
"call/ongoing": "conversation",
|
||||
@@ -90,6 +127,11 @@ loop.webapp = (function($, TB) {
|
||||
}
|
||||
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);
|
||||
|
||||
@@ -126,7 +168,10 @@ loop.webapp = (function($, TB) {
|
||||
*/
|
||||
initiate: function(loopToken) {
|
||||
this._conversation.set("loopToken", loopToken);
|
||||
this.loadView(new ConversationFormView({model: this._conversation}));
|
||||
this.loadView(new ConversationFormView({
|
||||
model: this._conversation,
|
||||
notifier: this._notifier
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
@@ -139,15 +184,17 @@ loop.webapp = (function($, TB) {
|
||||
if (loopToken) {
|
||||
return this.navigate("call/" + loopToken, {trigger: true});
|
||||
} else {
|
||||
// XXX: notify user that a call token is missing
|
||||
this._notifier.notify({
|
||||
message: __("Missing conversation information"),
|
||||
level: "error"
|
||||
});
|
||||
return this.navigate("home", {trigger: true});
|
||||
}
|
||||
}
|
||||
this.loadView(
|
||||
new sharedViews.ConversationView({
|
||||
sdk: TB,
|
||||
model: this._conversation
|
||||
}));
|
||||
this.loadView(new sharedViews.ConversationView({
|
||||
sdk: TB,
|
||||
model: this._conversation
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@@ -156,14 +203,18 @@ loop.webapp = (function($, TB) {
|
||||
*/
|
||||
function init() {
|
||||
conversation = new sharedModels.ConversationModel();
|
||||
router = new WebappRouter({conversation: conversation});
|
||||
router = new WebappRouter({
|
||||
conversation: conversation,
|
||||
notifier: new sharedViews.NotificationListView({el: "#messages"})
|
||||
});
|
||||
Backbone.history.start();
|
||||
}
|
||||
|
||||
return {
|
||||
baseServerUrl: baseServerUrl,
|
||||
ConversationFormView: ConversationFormView,
|
||||
HomeView: HomeView,
|
||||
init: init,
|
||||
WebappRouter: WebappRouter
|
||||
};
|
||||
})(jQuery, window.TB);
|
||||
})(jQuery, window.TB, document.webL10n);
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
[en]
|
||||
missing_conversation_info=Missing conversation information.
|
||||
unable_retrieve_call_info=Unable to retrieve conversation information.
|
||||
start=Start
|
||||
stop=Stop
|
||||
start_call=Start the call
|
||||
welcome=Welcome to the Loop web client.
|
||||
|
||||
[fr]
|
||||
missing_conversation_info=Informations de communication manquantes.
|
||||
unable_retrieve_call_info=Impossible de récupérer les informations liées à cet appel.
|
||||
start=Démarrer
|
||||
stop=Arrêter
|
||||
start_call=Démarrer l'appel
|
||||
|
||||
@@ -111,26 +111,79 @@ describe("loop.shared.views", function() {
|
||||
});
|
||||
|
||||
describe("NotificationListView", function() {
|
||||
describe("Collection events", function() {
|
||||
var coll, testNotif, view;
|
||||
var coll, notifData, testNotif;
|
||||
|
||||
beforeEach(function() {
|
||||
notifData = {level: "error", message: "plop"};
|
||||
testNotif = new sharedModels.NotificationModel(notifData);
|
||||
coll = new sharedModels.NotificationCollection();
|
||||
});
|
||||
|
||||
describe("#initialize", function() {
|
||||
it("should accept a collection option", function() {
|
||||
var view = new sharedViews.NotificationListView({collection: coll});
|
||||
|
||||
expect(view.collection).to.be.an.instanceOf(
|
||||
sharedModels.NotificationCollection);
|
||||
});
|
||||
|
||||
it("should set a default collection when none is passed", function() {
|
||||
var view = new sharedViews.NotificationListView();
|
||||
|
||||
expect(view.collection).to.be.an.instanceOf(
|
||||
sharedModels.NotificationCollection);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#clear", function() {
|
||||
it("should clear all notifications from the collection", function() {
|
||||
var view = new sharedViews.NotificationListView();
|
||||
view.notify(testNotif);
|
||||
|
||||
view.clear();
|
||||
|
||||
expect(coll).to.have.length.of(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#notify", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
|
||||
testNotif = new sharedModels.NotificationModel({
|
||||
level: "error",
|
||||
message: "plop"
|
||||
});
|
||||
coll = new sharedModels.NotificationCollection();
|
||||
view = new sharedViews.NotificationListView({collection: coll});
|
||||
});
|
||||
|
||||
it("should render on notification added to the collection", function() {
|
||||
coll.add(testNotif);
|
||||
describe("adds a new notification to the stack", function() {
|
||||
it("using a plain object", function() {
|
||||
view.notify(notifData);
|
||||
|
||||
sinon.assert.calledOnce(view.render);
|
||||
expect(coll).to.have.length.of(1);
|
||||
});
|
||||
|
||||
it("using a NotificationModel instance", function() {
|
||||
view.notify(testNotif);
|
||||
|
||||
expect(coll).to.have.length.of(1);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Collection events", function() {
|
||||
var view;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(sharedViews.NotificationListView.prototype, "render");
|
||||
view = new sharedViews.NotificationListView({collection: coll});
|
||||
});
|
||||
|
||||
it("should render on notification removed from the collection",
|
||||
it("should render when a notification is added to the collection",
|
||||
function() {
|
||||
coll.add(testNotif);
|
||||
|
||||
sinon.assert.calledOnce(view.render);
|
||||
});
|
||||
|
||||
it("should render when a notification is removed from the collection",
|
||||
function() {
|
||||
coll.add(testNotif);
|
||||
coll.remove(testNotif);
|
||||
@@ -138,12 +191,11 @@ describe("loop.shared.views", function() {
|
||||
sinon.assert.calledTwice(view.render);
|
||||
});
|
||||
|
||||
it("should render on collection reset",
|
||||
function() {
|
||||
coll.reset();
|
||||
it("should render when the collection is reset", function() {
|
||||
coll.reset();
|
||||
|
||||
sinon.assert.calledOnce(view.render);
|
||||
});
|
||||
sinon.assert.calledOnce(view.render);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -21,9 +21,10 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
describe("WebappRouter", function() {
|
||||
var conversation, fakeSessionData;
|
||||
var conversation, notifier, fakeSessionData;
|
||||
|
||||
beforeEach(function() {
|
||||
notifier = {notify: sandbox.spy()};
|
||||
conversation = new sharedModels.ConversationModel();
|
||||
fakeSessionData = {
|
||||
sessionId: "sessionId",
|
||||
@@ -38,6 +39,12 @@ describe("loop.webapp", function() {
|
||||
new loop.webapp.WebappRouter();
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
});
|
||||
|
||||
it("should require a notifier", function() {
|
||||
expect(function() {
|
||||
new loop.webapp.WebappRouter({conversation: {}});
|
||||
}).to.Throw(Error, /missing required notifier/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
@@ -57,7 +64,10 @@ describe("loop.webapp", function() {
|
||||
var router;
|
||||
|
||||
beforeEach(function() {
|
||||
router = new loop.webapp.WebappRouter({conversation: conversation});
|
||||
router = new loop.webapp.WebappRouter({
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
sandbox.stub(router, "loadView");
|
||||
});
|
||||
|
||||
@@ -125,7 +135,8 @@ describe("loop.webapp", function() {
|
||||
function() {
|
||||
sandbox.stub(loop.webapp.WebappRouter.prototype, "navigate");
|
||||
var router = new loop.webapp.WebappRouter({
|
||||
conversation: conversation
|
||||
conversation: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
|
||||
conversation.setReady(fakeSessionData);
|
||||
@@ -137,12 +148,30 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
|
||||
describe("ConversationFormView", function() {
|
||||
describe("#initialize", function() {
|
||||
it("should require a conversation option", function() {
|
||||
expect(function() {
|
||||
new loop.webapp.WebappRouter();
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
});
|
||||
|
||||
it("should require a notifier option", function() {
|
||||
expect(function() {
|
||||
new loop.webapp.WebappRouter({conversation: {}});
|
||||
}).to.Throw(Error, /missing required notifier/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#initiate", function() {
|
||||
var conversation, initiate, view, fakeSubmitEvent;
|
||||
var notifier, conversation, initiate, view, fakeSubmitEvent;
|
||||
|
||||
beforeEach(function() {
|
||||
notifier = {notify: sandbox.spy()};
|
||||
conversation = new sharedModels.ConversationModel();
|
||||
view = new loop.webapp.ConversationFormView({model: conversation});
|
||||
view = new loop.webapp.ConversationFormView({
|
||||
model: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
fakeSubmitEvent = {preventDefault: sinon.spy()};
|
||||
});
|
||||
|
||||
@@ -154,13 +183,37 @@ describe("loop.webapp", function() {
|
||||
|
||||
sinon.assert.calledOnce(fakeSubmitEvent.preventDefault);
|
||||
sinon.assert.calledOnce(initiate);
|
||||
// XXX host should be configurable
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=987086
|
||||
sinon.assert.calledWith(initiate, {
|
||||
baseServerUrl: "http://localhost:5000",
|
||||
baseServerUrl: loop.webapp.baseServerUrl,
|
||||
outgoing: true
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
var notifier, conversation, view;
|
||||
|
||||
beforeEach(function() {
|
||||
notifier = {notify: sandbox.spy()};
|
||||
conversation = new sharedModels.ConversationModel({
|
||||
loopToken: "fake"
|
||||
});
|
||||
view = new loop.webapp.ConversationFormView({
|
||||
model: conversation,
|
||||
notifier: notifier
|
||||
});
|
||||
});
|
||||
|
||||
it("should trigger a notication when a session:error model event is " +
|
||||
" received", function() {
|
||||
conversation.trigger("session:error", "tech error");
|
||||
|
||||
sinon.assert.calledOnce(notifier.notify);
|
||||
// XXX We should test for the actual message content, but webl10n gets
|
||||
// in the way as translated messages are all empty because matching
|
||||
// DOM elements are missing.
|
||||
sinon.assert.calledWithMatch(notifier.notify, {level: "error"});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user