Bug 985596 Set up initial desktop conversation window. r=dmose

This commit is contained in:
Mark Banner 2014-05-29 21:13:43 +01:00
parent 8711ed2ba4
commit b1a2bb524e
10 changed files with 293 additions and 255 deletions

View File

@ -10,10 +10,24 @@
<link rel="stylesheet" type="text/css" href="shared/css/conversation.css">
</head>
<body onload="loop.conversation.init();">
<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="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="js/client.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>
<script type="text/javascript" src="js/conversation.js"></script>
</body>
</html>

View File

@ -1,113 +0,0 @@
/* 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*/
var loop = loop || {};
loop.Client = (function($) {
"use strict";
/**
* Loop server client.
*
* @param {Object} settings Settings object.
*/
function Client(settings) {
settings = settings || {};
if (!settings.hasOwnProperty("baseApiUrl")) {
throw new Error("missing required baseApiUrl");
}
this.settings = settings;
}
Client.prototype = {
/**
* Requests a call URL to the Loop server.
*
* @param {String} simplepushUrl a registered Simple Push URL
* @param {string} nickname the nickname of the future caller
* @param {Function} cb Callback(err, callUrl)
*/
requestCallUrl: function(nickname, cb) {
var endpoint = this.settings.baseApiUrl + "/call-url/",
reqData = {callerId: nickname};
function validate(callUrlData) {
if (typeof callUrlData !== "object" ||
!callUrlData.hasOwnProperty("call_url")) {
var message = "Invalid call url data received";
console.error(message, callUrlData);
throw new Error(message);
}
return callUrlData.call_url;
}
var req = $.post(endpoint, reqData, function(callUrlData) {
try {
cb(null, validate(callUrlData));
} catch (err) {
cb(err);
}
}, "json");
req.fail(function(jqXHR, testStatus, errorThrown) {
var error = "Unknown error.";
if (jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.error) {
error = jqXHR.responseJSON.error;
}
var message = "HTTP error " + jqXHR.status + ": " +
errorThrown + "; " + error;
console.error(message);
cb(new Error(message));
});
},
/**
* Requests call information from the server
*
* @param {Function} cb Callback(err, calls)
*/
requestCallsInfo: function(cb) {
var endpoint = this.settings.baseApiUrl + "/calls";
// We do a basic validation here that we have an object,
// and pass the full information back.
function validate(callsData) {
if (typeof callsData !== "object" ||
!callsData.hasOwnProperty("calls")) {
var message = "Invalid calls data received";
console.error(message, callsData);
throw new Error(message);
}
return callsData.calls;
}
// XXX We'll want to figure out a way to store the version from each
// request here. As this is typically the date, we just need to store the
// time last requested.
// XXX It is likely that we'll want to move some of this to whatever
// opens the chat window.
var req = $.get(endpoint + "?version=0", function(callsData) {
try {
cb(null, validate(callsData));
} catch (err) {
cb(err);
}
}, "json");
req.fail(function(jqXHR, testStatus, errorThrown) {
var error = "Unknown error.";
if (jqXHR && jqXHR.responseJSON && jqXHR.responseJSON.error) {
error = jqXHR.responseJSON.error;
}
var message = "HTTP error " + jqXHR.status + ": " +
errorThrown + "; " + error;
console.error(message);
cb(new Error(message));
});
}
};
return Client;
})(jQuery);

View File

@ -3,36 +3,130 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*global loop*/
// XXX This file needs unit testing
Components.utils.import("resource://gre/modules/Services.jsm");
var loop = loop || {};
loop.conversation = (function(_, __) {
loop.conversation = (function(TB) {
"use strict";
// XXX: baseApiUrl should be configurable (browser pref)
var baseApiUrl = "http://localhost:5000";
var baseServerUrl = Services.prefs.getCharPref("loop.server");
/**
* App router.
* @type {loop.webapp.Router}
*/
var router;
/**
* Current conversation model instance.
* @type {loop.webapp.ConversationModel}
*/
var conversation;
var ConversationRouter = loop.shared.router.BaseRouter.extend({
_conversation: undefined,
activeView: undefined,
routes: {
"start/:version": "start",
"call/ongoing": "conversation",
"call/ended": "ended"
},
/**
* Loads and render current active view.
*
* @param {loop.shared.BaseView} view View.
*/
loadView : function(view) {
if (this.activeView) {
this.activeView.hide();
}
this.activeView = view.render().show();
},
initialize: function(options) {
options = options || {};
if (!options.conversation) {
throw new Error("missing required conversation");
}
this._conversation = options.conversation;
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() {
this.navigate("call/ongoing", {trigger: true});
},
/**
* Navigates to ended state when the call has ended
*/
_onSessionEnded: function() {
this.navigate("call/ended", {trigger: true});
},
/**
* start is the initial route that does any necessary prompting and set
* up for the call.
*
* @param {String} loopVersion The version from the push notification, set
* by the router from the URL.
*/
start: function(loopVersion) {
// XXX For now, we just kick the conversation straight away, bug 990678
// will implement the follow-ups.
this._conversation.set({loopVersion: loopVersion});
this._conversation.initiate({
baseServerUrl: baseServerUrl,
outgoing: false
});
},
/**
* 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");
return;
}
this.loadView(
new loop.shared.views.ConversationView({
sdk: TB,
model: this._conversation
}));
},
/**
* ended does any necessary work to end the call.
*/
ended: function() {
// XXX Later we implement the end-of call here (bug 974873)
window.close();
}
});
/**
* Panel initialisation.
*/
function init() {
// Send a message to the server to get the call info
this.client = new loop.Client({
baseApiUrl: baseApiUrl
});
// Get the call information
this.client.requestCallsInfo(function(err, calls) {
if (err) {
console.error("Error getting call data: ", err);
return;
}
console.log("Received Calls Data: ", calls);
});
conversation = new loop.shared.models.ConversationModel();
router = new ConversationRouter({conversation: conversation});
Backbone.history.start();
}
return {
ConversationRouter: ConversationRouter,
init: init
};
})(_);
})(window.TB);

View File

@ -3,13 +3,13 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/*global loop*/
Components.utils.import("resource://gre/modules/Services.jsm");
var loop = loop || {};
loop.panel = (function(_, __) {
"use strict";
// XXX: baseApiUrl should be configurable (browser pref)
var baseApiUrl = "http://localhost:5000",
var baseServerUrl = Services.prefs.getCharPref("loop.server"),
panelView;
/**
@ -98,8 +98,8 @@ loop.panel = (function(_, __) {
},
initialize: function() {
this.client = new loop.Client({
baseApiUrl: baseApiUrl
this.client = new loop.shared.Client({
baseServerUrl: baseServerUrl
});
this.notificationCollection = new NotificationCollection();
this.notificationListView = new NotificationListView({

View File

@ -44,7 +44,7 @@
<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="js/client.js"></script>
<script type="text/javascript" src="shared/js/client.js"></script>
<script type="text/javascript" src="js/panel.js"></script>
</body>
</html>

View File

@ -10,11 +10,15 @@ browser.jar:
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
content/browser/loop/shared/js/client.js (content/shared/js/client.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/router.js (content/shared/js/router.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
content/browser/loop/shared/libs/sdk.js (content/shared/libs/sdk.js)
content/browser/loop/shared/libs/lodash-2.4.1.js (content/shared/libs/lodash-2.4.1.js)
content/browser/loop/shared/libs/jquery-2.1.0.js (content/shared/libs/jquery-2.1.0.js)
content/browser/loop/shared/libs/backbone-1.1.2.js (content/shared/libs/backbone-1.1.2.js)
content/browser/loop/libs/l10n.js (content/libs/l10n.js)
content/browser/loop/js/fxcom.js (content/js/fxcom.js)
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/client.js (content/js/client.js)
content/browser/loop/js/panel.js (content/js/panel.js)

View File

@ -1,112 +0,0 @@
/* 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, sinon */
var expect = chai.expect;
describe("loop.Client", function() {
"use strict";
var sandbox, fakeXHR, requests = [];
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeXHR = sandbox.useFakeXMLHttpRequest();
requests = [];
// https://github.com/cjohansen/Sinon.JS/issues/393
fakeXHR.xhr.onCreate = function (xhr) {
requests.push(xhr);
};
});
afterEach(function() {
sandbox.restore();
});
describe("loop.Client", function() {
describe("#constructor", function() {
it("should require a baseApiUrl setting", function() {
expect(function() {
new loop.Client();
}).to.Throw(Error, /required/);
});
});
describe("#requestCallUrl", function() {
var client;
beforeEach(function() {
client = new loop.Client({baseApiUrl: "http://fake.api"});
});
it("should request for a call url", function() {
var callback = sinon.spy();
client.requestCallUrl("foo", callback);
expect(requests).to.have.length.of(1);
requests[0].respond(200, {"Content-Type": "application/json"},
'{"call_url": "fakeCallUrl"}');
sinon.assert.calledWithExactly(callback, null, "fakeCallUrl");
});
it("should send an error when the request fails", function() {
var callback = sinon.spy();
client.requestCallUrl("foo", callback);
expect(requests).to.have.length.of(1);
requests[0].respond(400, {"Content-Type": "application/json"},
'{"error": "my error"}');
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /HTTP error 400: Bad Request; my error/.test(err.message);
}));
});
});
describe("#requestCallsInfo", function() {
var client;
beforeEach(function() {
client = new loop.Client({baseApiUrl: "http://fake.api"});
});
it("should request data for all calls", function() {
var callback = sinon.spy();
client.requestCallsInfo(callback);
expect(requests).to.have.length.of(1);
expect(requests[0].url).to.be.equal("http://fake.api/calls?version=0");
expect(requests[0].method).to.be.equal("GET");
requests[0].respond(200, {"Content-Type": "application/json"},
'{"calls": [{"apiKey": "fake"}]}');
sinon.assert.calledWithExactly(callback, null, [{apiKey: "fake"}]);
});
it("should send an error when the request fails", function() {
var callback = sinon.spy();
client.requestCallsInfo(callback);
requests[0].respond(400, {"Content-Type": "application/json"},
'{"error": "my error"}');
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /HTTP error 400: Bad Request; my error/.test(err.message);
}));
});
it("should send an error if the data is not valid", function() {
var callback = sinon.spy();
client.requestCallsInfo(callback);
requests[0].respond(200, {"Content-Type": "application/json"},
'{"bad": {}}');
sinon.assert.calledWithMatch(callback, sinon.match(function(err) {
return /Invalid calls data received/.test(err.message);
}));
});
});
});
});

View File

@ -0,0 +1,140 @@
/* 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, sinon */
var expect = chai.expect;
describe("loop.conversation", function() {
"use strict";
var ConversationRouter = loop.conversation.ConversationRouter,
sandbox;
beforeEach(function() {
sandbox = sinon.sandbox.create();
});
afterEach(function() {
sandbox.restore();
});
describe("ConversationRouter", function() {
var conversation;
beforeEach(function() {
conversation = new loop.shared.models.ConversationModel();
});
describe("#constructor", function() {
it("should require a ConversationModel instance", function() {
expect(function() {
new ConversationRouter();
}).to.Throw(Error, /missing required conversation/);
});
});
describe("Routes", function() {
var router;
beforeEach(function() {
router = new ConversationRouter({conversation: conversation});
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");
});
it("should initiate the conversation", function() {
sandbox.stub(conversation, "initiate");
router.start("fakeVersion");
sinon.assert.calledOnce(conversation.initiate);
sinon.assert.calledWithExactly(conversation.initiate, {
baseServerUrl: "http://example.com",
outgoing: false
});
});
});
describe("#conversation", function() {
it("should load the ConversationView if session is set", function() {
sandbox.stub(loop.shared.views.ConversationView.prototype,
"initialize");
conversation.set("sessionId", "fakeSessionId");
router.conversation();
sinon.assert.calledOnce(router.loadView);
sinon.assert.calledWithMatch(router.loadView, {
$el: {selector: "#conversation"}
});
});
it("should not load the ConversationView if session is not set",
function() {
router.conversation();
sinon.assert.notCalled(router.loadView);
});
});
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");
router.ended();
sinon.assert.calledOnce(window.close);
});
});
});
describe("Events", function() {
var fakeSessionData;
beforeEach(function() {
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
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.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.trigger("session:ended");
sinon.assert.calledOnce(router.navigate);
sinon.assert.calledWith(router.navigate, "call/ended");
});
});
});
});

View File

@ -32,11 +32,16 @@
return [];
},
};
window.Components = {fake: true, utils: {import: function(){}}};
window.Services = {fake: true, prefs: {getCharPref: function() {
return "http://example.com";
}}};
</script>
<script src="../../content/libs/l10n.js"></script>
<script src="../../content/shared/libs/jquery-2.1.0.js"></script>
<script src="../../content/shared/libs/lodash-2.4.1.js"></script>
<script src="../../content/shared/libs/backbone-1.1.2.js"></script>
<!-- test dependencies -->
<script src="../shared/vendor/mocha-1.17.1.js"></script>
<script src="../shared/vendor/chai-1.9.0.js"></script>
@ -45,16 +50,22 @@
chai.Assertion.includeStack = true;
mocha.setup('bdd');
</script>
<!-- App scripts -->
<script src="../../content/js/client.js"></script>
<script src="../../content/shared/js/client.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/router.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/js/conversation.js"></script>
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="conversation_test.js"></script>
<script src="panel_test.js"></script>
<script src="client_test.js"></script>
<script>
mocha.run(function () {
$("#mocha").append("<p>Complete.</p>");
});
</script>
</script>
</body>
</html>

View File

@ -106,7 +106,7 @@ describe("loop.panel", function() {
describe("#getCallurl", function() {
it("should request a call url to the server", function() {
var requestCallUrl = sandbox.stub(loop.Client.prototype,
var requestCallUrl = sandbox.stub(loop.shared.Client.prototype,
"requestCallUrl");
var view = new loop.panel.PanelView();