Bug 1102170 - Share a room url by email when Loop direct call fails. r=Standard8

This commit is contained in:
Nicolas Perriault 2014-12-10 22:51:53 +01:00
Родитель 437abd21ee
Коммит db9bde32fb
7 изменённых файлов: 120 добавлений и 54 удалений

Просмотреть файл

@ -27,6 +27,13 @@ loop.conversationViews = (function(mozL10n) {
return contact.email.find(e => e.pref) || contact.email[0]; return contact.email.find(e => e.pref) || contact.email[0];
} }
function _getContactDisplayName(contact) {
if (contact.name && contact.name[0]) {
return contact.name[0];
}
return _getPreferredEmail(contact).value;
}
/** /**
* Displays information about the call * Displays information about the call
* Caller avatar, name & conversation creation date * Caller avatar, name & conversation creation date
@ -107,14 +114,7 @@ loop.conversationViews = (function(mozL10n) {
}, },
render: function() { render: function() {
var contactName; var contactName = _getContactDisplayName(this.props.contact);
if (this.props.contact.name &&
this.props.contact.name[0]) {
contactName = this.props.contact.name[0];
} else {
contactName = _getPreferredEmail(this.props.contact).value;
}
document.title = contactName; document.title = contactName;
@ -262,7 +262,10 @@ loop.conversationViews = (function(mozL10n) {
emailLinkButtonDisabled: true emailLinkButtonDisabled: true
}); });
this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink()); this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
roomOwner: navigator.mozLoop.userProfile.email,
roomName: _getContactDisplayName(this.props.contact)
}));
}, },
render: function() { render: function() {

Просмотреть файл

@ -27,6 +27,13 @@ loop.conversationViews = (function(mozL10n) {
return contact.email.find(e => e.pref) || contact.email[0]; return contact.email.find(e => e.pref) || contact.email[0];
} }
function _getContactDisplayName(contact) {
if (contact.name && contact.name[0]) {
return contact.name[0];
}
return _getPreferredEmail(contact).value;
}
/** /**
* Displays information about the call * Displays information about the call
* Caller avatar, name & conversation creation date * Caller avatar, name & conversation creation date
@ -107,14 +114,7 @@ loop.conversationViews = (function(mozL10n) {
}, },
render: function() { render: function() {
var contactName; var contactName = _getContactDisplayName(this.props.contact);
if (this.props.contact.name &&
this.props.contact.name[0]) {
contactName = this.props.contact.name[0];
} else {
contactName = _getPreferredEmail(this.props.contact).value;
}
document.title = contactName; document.title = contactName;
@ -262,7 +262,10 @@ loop.conversationViews = (function(mozL10n) {
emailLinkButtonDisabled: true emailLinkButtonDisabled: true
}); });
this.props.dispatcher.dispatch(new sharedActions.FetchEmailLink()); this.props.dispatcher.dispatch(new sharedActions.FetchRoomEmailLink({
roomOwner: navigator.mozLoop.userProfile.email,
roomName: _getContactDisplayName(this.props.contact)
}));
}, },
render: function() { render: function() {

Просмотреть файл

@ -76,10 +76,12 @@ loop.shared.actions = (function() {
}), }),
/** /**
* Fetch a new call url from the server, intended to be sent over email when * Fetch a new room url from the server, intended to be sent over email when
* a contact can't be reached. * a contact can't be reached.
*/ */
FetchEmailLink: Action.define("fetchEmailLink", { FetchRoomEmailLink: Action.define("fetchRoomEmailLink", {
roomOwner: String,
roomName: String
}), }),
/** /**

Просмотреть файл

@ -210,7 +210,7 @@ loop.store = loop.store || {};
"retryCall", "retryCall",
"mediaConnected", "mediaConnected",
"setMute", "setMute",
"fetchEmailLink" "fetchRoomEmailLink"
]); ]);
this.setStoreState({ this.setStoreState({
@ -323,18 +323,21 @@ loop.store = loop.store || {};
}, },
/** /**
* Fetches a new call URL intended to be sent over email when a contact * Fetches a new room URL intended to be sent over email when a contact
* can't be reached. * can't be reached.
*/ */
fetchEmailLink: function() { fetchRoomEmailLink: function(actionData) {
// XXX This is an empty string as a conversation identifier. Bug 1015938 implements this.mozLoop.rooms.create({
// a user-set string. roomName: actionData.roomName,
this.client.requestCallUrl("", function(err, callUrlData) { roomOwner: actionData.roomOwner,
maxSize: loop.store.MAX_ROOM_CREATION_SIZE,
expiresIn: loop.store.DEFAULT_EXPIRES_IN
}, function(err, createdRoomData) {
if (err) { if (err) {
this.trigger("error:emailLink"); this.trigger("error:emailLink");
return; return;
} }
this.setStoreState({"emailLink": callUrlData.callUrl}); this.setStoreState({"emailLink": createdRoomData.roomUrl});
}.bind(this)); }.bind(this));
}, },

Просмотреть файл

@ -16,6 +16,20 @@ loop.store = loop.store || {};
*/ */
var sharedActions = loop.shared.actions; var sharedActions = loop.shared.actions;
/**
* Maximum size given to createRoom; only 2 is supported (and is
* always passed) because that's what the user-experience is currently
* designed and tested to handle.
* @type {Number}
*/
var MAX_ROOM_CREATION_SIZE = loop.store.MAX_ROOM_CREATION_SIZE = 2;
/**
* The number of hours for which the room will exist - default 8 weeks
* @type {Number}
*/
var DEFAULT_EXPIRES_IN = loop.store.DEFAULT_EXPIRES_IN = 24 * 7 * 8;
/** /**
* Room validation schema. See validate.js. * Room validation schema. See validate.js.
* @type {Object} * @type {Object}
@ -61,13 +75,13 @@ loop.store = loop.store || {};
* designed and tested to handle. * designed and tested to handle.
* @type {Number} * @type {Number}
*/ */
maxRoomCreationSize: 2, maxRoomCreationSize: MAX_ROOM_CREATION_SIZE,
/** /**
* The number of hours for which the room will exist - default 8 weeks * The number of hours for which the room will exist - default 8 weeks
* @type {Number} * @type {Number}
*/ */
defaultExpiresIn: 24 * 7 * 8, defaultExpiresIn: DEFAULT_EXPIRES_IN,
/** /**
* Registered actions. * Registered actions.

Просмотреть файл

@ -56,7 +56,10 @@ describe("loop.conversationViews", function () {
}, },
getAudioBlob: sinon.spy(function(name, callback) { getAudioBlob: sinon.spy(function(name, callback) {
callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"})); callback(null, new Blob([new ArrayBuffer(10)], {type: "audio/ogg"}));
}) }),
userProfile: {
email: "bob@invalid.tld"
}
}; };
fakeWindow = { fakeWindow = {
@ -241,12 +244,15 @@ describe("loop.conversationViews", function () {
describe("CallFailedView", function() { describe("CallFailedView", function() {
var store, fakeAudio; var store, fakeAudio;
function mountTestComponent(props) { var contact = {email: [{value: "test@test.tld"}]};
function mountTestComponent(options) {
options = options || {};
return TestUtils.renderIntoDocument( return TestUtils.renderIntoDocument(
loop.conversationViews.CallFailedView({ loop.conversationViews.CallFailedView({
dispatcher: dispatcher, dispatcher: dispatcher,
store: store, store: store,
contact: {email: [{value: "test@test.tld"}]} contact: options.contact
})); }));
} }
@ -266,7 +272,7 @@ describe("loop.conversationViews", function () {
it("should dispatch a retryCall action when the retry button is pressed", it("should dispatch a retryCall action when the retry button is pressed",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
var retryBtn = view.getDOMNode().querySelector('.btn-retry'); var retryBtn = view.getDOMNode().querySelector('.btn-retry');
@ -279,7 +285,7 @@ describe("loop.conversationViews", function () {
it("should dispatch a cancelCall action when the cancel button is pressed", it("should dispatch a cancelCall action when the cancel button is pressed",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
var cancelBtn = view.getDOMNode().querySelector('.btn-cancel'); var cancelBtn = view.getDOMNode().querySelector('.btn-cancel');
@ -290,9 +296,9 @@ describe("loop.conversationViews", function () {
sinon.match.hasOwn("name", "cancelCall")); sinon.match.hasOwn("name", "cancelCall"));
}); });
it("should dispatch a fetchEmailLink action when the cancel button is pressed", it("should dispatch a fetchRoomEmailLink action when the email button is pressed",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email'); var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
@ -300,12 +306,32 @@ describe("loop.conversationViews", function () {
sinon.assert.calledOnce(dispatcher.dispatch); sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch, sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("name", "fetchEmailLink")); sinon.match.hasOwn("name", "fetchRoomEmailLink"));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("roomOwner", fakeMozLoop.userProfile.email));
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("roomName", "test@test.tld"));
});
it("should name the created room using the contact name when available",
function() {
view = mountTestComponent({contact: {
email: [{value: "test@test.tld"}],
name: ["Mr Fake ContactName"]
}});
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
React.addons.TestUtils.Simulate.click(emailLinkBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
sinon.match.hasOwn("roomName", "Mr Fake ContactName"));
}); });
it("should disable the email link button once the action is dispatched", it("should disable the email link button once the action is dispatched",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
var emailLinkBtn = view.getDOMNode().querySelector('.btn-email'); var emailLinkBtn = view.getDOMNode().querySelector('.btn-email');
React.addons.TestUtils.Simulate.click(emailLinkBtn); React.addons.TestUtils.Simulate.click(emailLinkBtn);
@ -314,7 +340,7 @@ describe("loop.conversationViews", function () {
it("should compose an email once the email link is received", function() { it("should compose an email once the email link is received", function() {
var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail"); var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
view = mountTestComponent(); view = mountTestComponent({contact: contact});
store.setStoreState({emailLink: "http://fake.invalid/"}); store.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(composeCallUrlEmail); sinon.assert.calledOnce(composeCallUrlEmail);
@ -324,7 +350,7 @@ describe("loop.conversationViews", function () {
it("should close the conversation window once the email link is received", it("should close the conversation window once the email link is received",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
store.setStoreState({emailLink: "http://fake.invalid/"}); store.setStoreState({emailLink: "http://fake.invalid/"});
@ -333,7 +359,7 @@ describe("loop.conversationViews", function () {
it("should display an error message in case email link retrieval failed", it("should display an error message in case email link retrieval failed",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
store.trigger("error:emailLink"); store.trigger("error:emailLink");
@ -342,7 +368,7 @@ describe("loop.conversationViews", function () {
it("should allow retrying to get a call url if it failed previously", it("should allow retrying to get a call url if it failed previously",
function() { function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
store.trigger("error:emailLink"); store.trigger("error:emailLink");
@ -350,7 +376,7 @@ describe("loop.conversationViews", function () {
}); });
it("should play a failure sound, once", function() { it("should play a failure sound, once", function() {
view = mountTestComponent(); view = mountTestComponent({contact: contact});
sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob); sinon.assert.calledOnce(navigator.mozLoop.getAudioBlob);
sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob, sinon.assert.calledWithExactly(navigator.mozLoop.getAudioBlob,

Просмотреть файл

@ -42,6 +42,9 @@ describe("loop.store.ConversationStore", function () {
calls: { calls: {
setCallInProgress: sandbox.stub(), setCallInProgress: sandbox.stub(),
clearCallInProgress: sandbox.stub() clearCallInProgress: sandbox.stub()
},
rooms: {
create: sandbox.stub()
} }
}; };
@ -701,20 +704,29 @@ describe("loop.store.ConversationStore", function () {
}); });
}); });
describe("#fetchEmailLink", function() { describe("#fetchRoomEmailLink", function() {
it("should request a new call url to the server", function() { it("should request a new call url to the server", function() {
store.fetchEmailLink(new sharedActions.FetchEmailLink()); store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
roomOwner: "bob@invalid.tld",
roomName: "FakeRoomName"
}));
sinon.assert.calledOnce(client.requestCallUrl); sinon.assert.calledOnce(fakeMozLoop.rooms.create);
sinon.assert.calledWith(client.requestCallUrl, ""); sinon.assert.calledWithMatch(fakeMozLoop.rooms.create, {
roomOwner: "bob@invalid.tld",
roomName: "FakeRoomName"
});
}); });
it("should update the emailLink attribute when the new call url is received", it("should update the emailLink attribute when the new room url is received",
function() { function() {
client.requestCallUrl = function(callId, cb) { fakeMozLoop.rooms.create = function(roomData, cb) {
cb(null, {callUrl: "http://fake.invalid/"}); cb(null, {roomUrl: "http://fake.invalid/"});
}; };
store.fetchEmailLink(new sharedActions.FetchEmailLink()); store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
roomOwner: "bob@invalid.tld",
roomName: "FakeRoomName"
}));
expect(store.getStoreState("emailLink")).eql("http://fake.invalid/"); expect(store.getStoreState("emailLink")).eql("http://fake.invalid/");
}); });
@ -722,10 +734,13 @@ describe("loop.store.ConversationStore", function () {
it("should trigger an error:emailLink event in case of failure", it("should trigger an error:emailLink event in case of failure",
function() { function() {
var trigger = sandbox.stub(store, "trigger"); var trigger = sandbox.stub(store, "trigger");
client.requestCallUrl = function(callId, cb) { fakeMozLoop.rooms.create = function(roomData, cb) {
cb("error"); cb(new Error("error"));
}; };
store.fetchEmailLink(new sharedActions.FetchEmailLink()); store.fetchRoomEmailLink(new sharedActions.FetchRoomEmailLink({
roomOwner: "bob@invalid.tld",
roomName: "FakeRoomName"
}));
sinon.assert.calledOnce(trigger); sinon.assert.calledOnce(trigger);
sinon.assert.calledWithExactly(trigger, "error:emailLink"); sinon.assert.calledWithExactly(trigger, "error:emailLink");