Bug 1065203 - Add some sound notifications for Loop's standalone link-clicker ui. r=nperriault

This commit is contained in:
Romain Gauthier 2014-10-27 17:05:09 +00:00
parent 34d549e888
commit 9a1c8be5b2
11 changed files with 188 additions and 34 deletions

View File

@ -97,7 +97,58 @@ loop.shared.mixins = (function() {
}
};
/**
* Audio mixin. Allows playing a single audio file and ensuring it
* is stopped when the component is unmounted.
*/
var AudioMixin = {
audio: null,
_isLoopDesktop: function() {
return typeof rootObject.navigator.mozLoop === "object";
},
/**
* Starts playing an audio file, stopping any audio that is already in progress.
*
* @param {String} filename The filename to play (excluding the extension).
*/
play: function(filename, options) {
if (this._isLoopDesktop()) {
// XXX: We need navigator.mozLoop.playSound(name), see Bug 1089585.
return;
}
options = options || {};
options.loop = options.loop || false;
this._ensureAudioStopped();
this.audio = new Audio('shared/sounds/' + filename + ".ogg");
this.audio.loop = options.loop;
this.audio.play();
},
/**
* Ensures audio is stopped playing, and removes the object from memory.
*/
_ensureAudioStopped: function() {
if (this.audio) {
this.audio.pause();
this.audio.removeAttribute("src");
delete this.audio;
}
},
/**
* Ensures audio is stopped when the component is unmounted.
*/
componentWillUnmount: function() {
this._ensureAudioStopped();
}
};
return {
AudioMixin: AudioMixin,
setRootObject: setRootObject,
DropdownMenuMixin: DropdownMenuMixin,
DocumentVisibilityMixin: DocumentVisibilityMixin

View File

@ -135,7 +135,7 @@ loop.shared.views = (function(_, OT, l10n) {
* Conversation view.
*/
var ConversationView = React.createClass({displayName: 'ConversationView',
mixins: [Backbone.Events],
mixins: [Backbone.Events, sharedMixins.AudioMixin],
propTypes: {
sdk: React.PropTypes.object.isRequired,
@ -183,7 +183,7 @@ loop.shared.views = (function(_, OT, l10n) {
componentDidMount: function() {
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this._onSessionConnected);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
@ -225,6 +225,11 @@ loop.shared.views = (function(_, OT, l10n) {
this.props.model.endSession();
},
_onSessionConnected: function(event) {
this.startPublishing(event);
this.play("connected");
},
/**
* Subscribes and attaches each created stream to a DOM element.
*

View File

@ -135,7 +135,7 @@ loop.shared.views = (function(_, OT, l10n) {
* Conversation view.
*/
var ConversationView = React.createClass({
mixins: [Backbone.Events],
mixins: [Backbone.Events, sharedMixins.AudioMixin],
propTypes: {
sdk: React.PropTypes.object.isRequired,
@ -183,7 +183,7 @@ loop.shared.views = (function(_, OT, l10n) {
componentDidMount: function() {
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this._onSessionConnected);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
@ -225,6 +225,11 @@ loop.shared.views = (function(_, OT, l10n) {
this.props.model.endSession();
},
_onSessionConnected: function(event) {
this.startPublishing(event);
this.play("connected");
},
/**
* Subscribes and attaches each created stream to a DOM element.
*

View File

@ -262,9 +262,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
callState: "connecting"
};
},
@ -274,11 +277,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
componentDidMount: function() {
this.play("connecting", {loop: true});
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.play("ringing", {loop: true});
this.setState({callState: "ringing"});
},
@ -518,6 +523,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Ended conversation view.
*/
var EndedConversationView = React.createClass({displayName: 'EndedConversationView',
mixins: [sharedMixins.AudioMixin],
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
@ -526,6 +533,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("terminated");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),

View File

@ -262,9 +262,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
var PendingConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
callState: "connecting"
};
},
@ -274,11 +277,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
componentDidMount: function() {
this.play("connecting", {loop: true});
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
this._handleRingingProgress);
},
_handleRingingProgress: function() {
this.play("ringing", {loop: true});
this.setState({callState: "ringing"});
},
@ -518,6 +523,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
* Ended conversation view.
*/
var EndedConversationView = React.createClass({
mixins: [sharedMixins.AudioMixin],
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
@ -526,6 +533,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
componentDidMount: function() {
this.play("terminated");
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),

View File

@ -161,13 +161,20 @@ describe("loop.shared.views", function() {
});
describe("ConversationView", function() {
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model;
var fakeSDK, fakeSessionData, fakeSession, fakePublisher, model, fakeAudio;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(sharedViews.ConversationView(props));
}
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
fakeSessionData = {
sessionId: "sessionId",
sessionToken: "sessionToken",
@ -350,46 +357,69 @@ describe("loop.shared.views", function() {
});
describe("Model events", function() {
it("should start streaming on session:connected", function() {
model.trigger("session:connected");
sinon.assert.calledOnce(fakeSDK.initPublisher);
});
describe("for standalone", function() {
it("should publish remote stream on session:stream-created",
function() {
var s1 = {connection: {connectionId: 42}};
model.trigger("session:stream-created", {stream: s1});
sinon.assert.calledOnce(fakeSession.subscribe);
sinon.assert.calledWith(fakeSession.subscribe, s1);
beforeEach(function() {
// In standalone, navigator.mozLoop does not exists
if (navigator.hasOwnProperty("mozLoop"))
sandbox.stub(navigator, "mozLoop", undefined);
});
it("should unpublish local stream on session:ended", function() {
comp.startPublishing();
it("should play a connected sound, once, on session:connected",
function() {
model.trigger("session:connected");
model.trigger("session:ended");
sinon.assert.calledOnce(fakeSession.unpublish);
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(
window.Audio, "shared/sounds/connected.ogg");
expect(fakeAudio.loop).to.not.equal(true);
});
});
it("should unpublish local stream on session:peer-hungup", function() {
comp.startPublishing();
describe("for both (standalone and desktop)", function() {
it("should start streaming on session:connected", function() {
model.trigger("session:connected");
model.trigger("session:peer-hungup");
sinon.assert.calledOnce(fakeSDK.initPublisher);
});
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should publish remote stream on session:stream-created",
function() {
var s1 = {connection: {connectionId: 42}};
it("should unpublish local stream on session:network-disconnected",
function() {
model.trigger("session:stream-created", {stream: s1});
sinon.assert.calledOnce(fakeSession.subscribe);
sinon.assert.calledWith(fakeSession.subscribe, s1);
});
it("should unpublish local stream on session:ended", function() {
comp.startPublishing();
model.trigger("session:network-disconnected");
model.trigger("session:ended");
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unpublish local stream on session:peer-hungup", function() {
comp.startPublishing();
model.trigger("session:peer-hungup");
sinon.assert.calledOnce(fakeSession.unpublish);
});
it("should unpublish local stream on session:network-disconnected",
function() {
comp.startPublishing();
model.trigger("session:network-disconnected");
sinon.assert.calledOnce(fakeSession.unpublish);
});
});
});
describe("Publisher events", function() {

View File

@ -575,7 +575,7 @@ describe("loop.webapp", function() {
});
describe("PendingConversationView", function() {
var view, websocket;
var view, websocket, fakeAudio;
beforeEach(function() {
websocket = new loop.CallConnectionWebSocket({
@ -585,6 +585,12 @@ describe("loop.webapp", function() {
});
sinon.stub(websocket, "cancel");
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.PendingConversationView({
@ -593,6 +599,16 @@ describe("loop.webapp", function() {
);
});
describe("#componentDidMount", function() {
it("should play a looped connecting sound", function() {
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/connecting.ogg");
expect(fakeAudio.loop).to.equal(true);
});
});
describe("#_cancelOutgoingCall", function() {
it("should inform the websocket to cancel the setup", function() {
var button = view.getDOMNode().querySelector(".btn-cancel");
@ -609,6 +625,13 @@ describe("loop.webapp", function() {
expect(view.state.callState).to.be.equal("ringing");
});
it("should play a looped ringing sound", function() {
websocket.trigger("progress:alerting");
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/ringing.ogg");
expect(fakeAudio.loop).to.equal(true);
});
});
});
});
@ -843,9 +866,16 @@ describe("loop.webapp", function() {
});
describe("EndedConversationView", function() {
var view, conversation;
var view, conversation, fakeAudio;
beforeEach(function() {
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
conversation = new sharedModels.ConversationModel({}, {
sdk: {}
});
@ -866,6 +896,17 @@ describe("loop.webapp", function() {
it("should render a FeedbackView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
});
describe("#componentDidMount", function() {
it("should play a terminating sound, once", function() {
sinon.assert.calledOnce(window.Audio);
sinon.assert.calledWithExactly(window.Audio, "shared/sounds/terminated.ogg");
expect(fakeAudio.loop).to.not.equal(true);
});
});
});
describe("PromoteFirefoxView", function() {