@@ -964,6 +965,33 @@ loop.conversationViews = (function(mozL10n) {
);
},
+ _renderViewFromCallType: function() {
+ // For outgoing calls we can display the pending conversation view
+ // for any state that render() doesn't manage.
+ if (this.state.outgoing) {
+ return (
);
+ }
+
+ // For incoming calls that are in accepting state, display the
+ // accept call view.
+ if (this.state.callState === CALL_STATES.ALERTING) {
+ return (
);
+ }
+
+ // Otherwise we're still gathering or connecting, so
+ // don't display anything.
+ return null;
+ },
+
render: function() {
switch (this.state.callState) {
case CALL_STATES.CLOSE: {
@@ -993,12 +1021,7 @@ loop.conversationViews = (function(mozL10n) {
return null;
}
default: {
- return (
);
+ return this._renderViewFromCallType();
}
}
},
diff --git a/browser/components/loop/content/shared/js/conversationStore.js b/browser/components/loop/content/shared/js/conversationStore.js
index 0cf590302604..3d364a2faa20 100644
--- a/browser/components/loop/content/shared/js/conversationStore.js
+++ b/browser/components/loop/content/shared/js/conversationStore.js
@@ -233,17 +233,26 @@ loop.store = loop.store || {};
]);
this.setStoreState({
+ apiKey: actionData.apiKey,
+ callerId: actionData.callerId,
+ callId: actionData.callId,
+ callState: CALL_STATES.GATHER,
+ callType: actionData.callType,
contact: actionData.contact,
outgoing: windowType === "outgoing",
- windowId: actionData.windowId,
- callType: actionData.callType,
- callState: CALL_STATES.GATHER,
- videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY
+ progressURL: actionData.progressURL,
+ sessionId: actionData.sessionId,
+ sessionToken: actionData.sessionToken,
+ videoMuted: actionData.callType === CALL_TYPES.AUDIO_ONLY,
+ websocketToken: actionData.websocketToken,
+ windowId: actionData.windowId
});
if (this.getStoreState("outgoing")) {
this._setupOutgoingCall();
- } // XXX Else, other types aren't supported yet.
+ } else {
+ this._setupIncomingCall();
+ }
},
/**
@@ -292,15 +301,19 @@ loop.store = loop.store || {};
},
/**
- * Cancels a call
+ * Cancels a call. This can happen for incoming or outgoing calls.
+ * Although the user doesn't "cancel" an incoming call, it may be that
+ * the remote peer cancelled theirs before the incoming call was accepted.
*/
cancelCall: function() {
- var callState = this.getStoreState("callState");
- if (this._websocket &&
- (callState === CALL_STATES.CONNECTING ||
- callState === CALL_STATES.ALERTING)) {
- // Let the server know the user has hung up.
- this._websocket.cancel();
+ if (this.getStoreState("outgoing")) {
+ var callState = this.getStoreState("callState");
+ if (this._websocket &&
+ (callState === CALL_STATES.CONNECTING ||
+ callState === CALL_STATES.ALERTING)) {
+ // Let the server know the user has hung up.
+ this._websocket.cancel();
+ }
}
this._endSession();
@@ -369,6 +382,15 @@ loop.store = loop.store || {};
this._endSession();
},
+ /**
+ * Sets up an incoming call. All we really need to do here is
+ * to connect the websocket, as we've already got all the information
+ * when the window opened.
+ */
+ _setupIncomingCall: function() {
+ this._connectWebSocket();
+ },
+
/**
* Obtains the outgoing call data from the server and handles the
* result.
@@ -466,29 +488,54 @@ loop.store = loop.store || {};
this.getStoreState("windowId"));
},
+ /**
+ * If we hit any of the termination reasons, and the user hasn't accepted
+ * then it seems reasonable to close the window/abort the incoming call.
+ *
+ * If the user has accepted the call, and something's happened, display
+ * the call failed view.
+ *
+ * https://wiki.mozilla.org/Loop/Architecture/MVP#Termination_Reasons
+ *
+ * For outgoing calls, we treat all terminations as failures.
+ *
+ * @param {Object} progressData The progress data received from the websocket.
+ * @param {String} previousState The previous state the websocket was in.
+ */
+ _handleWebSocketStateTerminated: function(progressData, previousState) {
+ if (this.getStoreState("outgoing") ||
+ (previousState !== WS_STATES.INIT &&
+ previousState !== WS_STATES.ALERTING)) {
+ // For outgoing calls we can treat everything as connection failure.
+ this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
+ reason: progressData.reason
+ }));
+ return;
+ }
+
+ this.dispatcher.dispatch(new sharedActions.CancelCall());
+ },
+
/**
* Used to handle any progressed received from the websocket. This will
* dispatch new actions so that the data can be handled appropriately.
+ *
+ * @param {Object} progressData The progress data received from the websocket.
+ * @param {String} previousState The previous state the websocket was in.
*/
- _handleWebSocketProgress: function(progressData) {
- var action;
-
+ _handleWebSocketProgress: function(progressData, previousState) {
switch(progressData.state) {
case WS_STATES.TERMINATED: {
- action = new sharedActions.ConnectionFailure({
- reason: progressData.reason
- });
+ this._handleWebSocketStateTerminated(progressData, previousState);
break;
}
default: {
- action = new sharedActions.ConnectionProgress({
+ this.dispatcher.dispatch(new sharedActions.ConnectionProgress({
wsState: progressData.state
- });
+ }));
break;
}
}
-
- this.dispatcher.dispatch(action);
}
});
})();
diff --git a/browser/components/loop/test/desktop-local/conversationViews_test.js b/browser/components/loop/test/desktop-local/conversationViews_test.js
index 76d98f9c730c..1c53dd60e1c1 100644
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -616,11 +616,12 @@ describe("loop.conversationViews", function () {
loop.conversationViews.CallFailedView);
});
- it("should render the PendingConversationView when the call state is 'gather'",
+ it("should render the PendingConversationView for outgoing calls when the call state is 'gather'",
function() {
store.setStoreState({
callState: CALL_STATES.GATHER,
- contact: contact
+ contact: contact,
+ outgoing: true
});
view = mountTestComponent();
@@ -629,6 +630,18 @@ describe("loop.conversationViews", function () {
loop.conversationViews.PendingConversationView);
});
+ it("should render the AcceptCallView for incoming calls when the call state is 'alerting'", function() {
+ store.setStoreState({
+ callState: CALL_STATES.ALERTING,
+ outgoing: false
+ });
+
+ view = mountTestComponent();
+
+ TestUtils.findRenderedComponentWithType(view,
+ loop.conversationViews.AcceptCallView);
+ });
+
it("should render the OngoingConversationView when the call state is 'ongoing'",
function() {
store.setStoreState({callState: CALL_STATES.ONGOING});
@@ -669,7 +682,8 @@ describe("loop.conversationViews", function () {
function() {
store.setStoreState({
callState: CALL_STATES.GATHER,
- contact: contact
+ contact: contact,
+ outgoing: true
});
view = mountTestComponent();
diff --git a/browser/components/loop/test/desktop-local/conversation_test.js b/browser/components/loop/test/desktop-local/conversation_test.js
index 4d7de1d2fccd..17d9c4243dcf 100644
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -129,27 +129,20 @@ describe("loop.conversation", function() {
});
describe("AppControllerView", function() {
- var conversationStore, conversation, client, ccView, oldTitle, dispatcher;
+ var conversationStore, client, ccView, oldTitle, dispatcher;
var conversationAppStore, roomStore;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversation.AppControllerView, {
- client: client,
- conversation: conversation,
roomStore: roomStore,
- sdk: {},
- dispatcher: dispatcher,
- mozLoop: navigator.mozLoop
+ dispatcher: dispatcher
}));
}
beforeEach(function() {
oldTitle = document.title;
client = new loop.Client();
- conversation = new loop.shared.models.ConversationModel({}, {
- sdk: {}
- });
dispatcher = new loop.Dispatcher();
conversationStore = new loop.store.ConversationStore(
dispatcher, {
@@ -195,20 +188,13 @@ describe("loop.conversation", function() {
loop.conversationViews.CallControllerView);
});
- it("should display the IncomingConversationView for incoming calls", function() {
- sandbox.stub(conversation, "setIncomingSessionData");
- sandbox.stub(loop, "CallConnectionWebSocket").returns({
- promiseConnect: function() {
- return new Promise(function() {});
- },
- on: sandbox.spy()
- });
+ it("should display the CallControllerView for incoming calls", function() {
conversationAppStore.setStoreState({windowType: "incoming"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
- loop.conversationViews.IncomingConversationView);
+ loop.conversationViews.CallControllerView);
});
it("should display the RoomView for rooms", function() {
diff --git a/browser/components/loop/test/shared/conversationStore_test.js b/browser/components/loop/test/shared/conversationStore_test.js
index 665e28a92324..2b985c76a4c6 100644
--- a/browser/components/loop/test/shared/conversationStore_test.js
+++ b/browser/components/loop/test/shared/conversationStore_test.js
@@ -299,6 +299,79 @@ describe("loop.store.ConversationStore", function () {
.eql(sharedUtils.CALL_TYPES.AUDIO_VIDEO);
});
+ describe("incoming calls", function() {
+ beforeEach(function() {
+ store.setStoreState({outgoing: false});
+ });
+
+ it("should initialize the websocket", function() {
+ sandbox.stub(loop, "CallConnectionWebSocket").returns({
+ promiseConnect: function() { return connectPromise; },
+ on: sinon.spy()
+ });
+
+ store.connectCall(
+ new sharedActions.ConnectCall({sessionData: fakeSessionData}));
+
+ sinon.assert.calledOnce(loop.CallConnectionWebSocket);
+ sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
+ url: "fakeURL",
+ callId: "142536",
+ websocketToken: "543216"
+ });
+ });
+
+ it("should connect the websocket to the server", function() {
+ store.connectCall(
+ new sharedActions.ConnectCall({sessionData: fakeSessionData}));
+
+ sinon.assert.calledOnce(store._websocket.promiseConnect);
+ });
+
+ describe("WebSocket connection result", function() {
+ beforeEach(function() {
+ store.connectCall(
+ new sharedActions.ConnectCall({sessionData: fakeSessionData}));
+
+ sandbox.stub(dispatcher, "dispatch");
+ });
+
+ it("should dispatch a connection progress action on success", function(done) {
+ resolveConnectPromise(WS_STATES.INIT);
+
+ connectPromise.then(function() {
+ checkFailures(done, function() {
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ // Can't use instanceof here, as that matches any action
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "connectionProgress"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("wsState", WS_STATES.INIT));
+ });
+ }, function() {
+ done(new Error("Promise should have been resolve, not rejected"));
+ });
+ });
+
+ it("should dispatch a connection failure action on failure", function(done) {
+ rejectConnectPromise();
+
+ connectPromise.then(function() {
+ done(new Error("Promise should have been rejected, not resolved"));
+ }, function() {
+ checkFailures(done, function() {
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ // Can't use instanceof here, as that matches any action
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("name", "connectionFailure"));
+ sinon.assert.calledWithMatch(dispatcher.dispatch,
+ sinon.match.hasOwn("reason", "websocket-setup"));
+ });
+ });
+ });
+ });
+ });
+
describe("outgoing calls", function() {
it("should request the outgoing call data", function() {
dispatcher.dispatch(
@@ -632,7 +705,9 @@ describe("loop.store.ConversationStore", function () {
sinon.assert.calledOnce(sdkDriver.disconnectSession);
});
- it("should send a cancel message to the websocket if it is open", function() {
+ it("should send a cancel message to the websocket if it is open for outgoing calls", function() {
+ store.setStoreState({outgoing: true});
+
store.cancelCall(new sharedActions.CancelCall());
sinon.assert.calledOnce(wsCancelSpy);
@@ -787,10 +862,14 @@ describe("loop.store.ConversationStore", function () {
sandbox.stub(dispatcher, "dispatch");
});
- it("should dispatch a connection failure action on 'terminate'", function() {
+ it("should dispatch a connection failure action on 'terminate' for outgoing calls", function() {
+ store.setStoreState({
+ outgoing: true
+ });
+
store._websocket.trigger("progress", {
state: WS_STATES.TERMINATED,
- reason: WEBSOCKET_REASONS.REJECT
+ reason: WEBSOCKET_REASONS.REJECT,
});
sinon.assert.calledOnce(dispatcher.dispatch);
@@ -801,6 +880,56 @@ describe("loop.store.ConversationStore", function () {
sinon.match.hasOwn("reason", WEBSOCKET_REASONS.REJECT));
});
+ it("should dispatch a connection failure action on 'terminate' for incoming calls if the previous state was not 'alerting' or 'init'", function() {
+ store.setStoreState({
+ outgoing: false
+ });
+
+ store._websocket.trigger("progress", {
+ state: WS_STATES.TERMINATED,
+ reason: WEBSOCKET_REASONS.CANCEL
+ }, WS_STATES.CONNECTING);
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ // Can't use instanceof here, as that matches any action
+ sinon.assert.calledWithExactly(dispatcher.dispatch,
+ new sharedActions.ConnectionFailure({
+ reason: WEBSOCKET_REASONS.CANCEL
+ }));
+ });
+
+ it("should dispatch a cancel call action on 'terminate' for incoming calls if the previous state was 'init'", function() {
+ store.setStoreState({
+ outgoing: false
+ });
+
+ store._websocket.trigger("progress", {
+ state: WS_STATES.TERMINATED,
+ reason: WEBSOCKET_REASONS.CANCEL
+ }, WS_STATES.INIT);
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ // Can't use instanceof here, as that matches any action
+ sinon.assert.calledWithExactly(dispatcher.dispatch,
+ new sharedActions.CancelCall({}));
+ });
+
+ it("should dispatch a cancel call action on 'terminate' for incoming calls if the previous state was 'alerting'", function() {
+ store.setStoreState({
+ outgoing: false
+ });
+
+ store._websocket.trigger("progress", {
+ state: WS_STATES.TERMINATED,
+ reason: WEBSOCKET_REASONS.CANCEL
+ }, WS_STATES.ALERTING);
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ // Can't use instanceof here, as that matches any action
+ sinon.assert.calledWithExactly(dispatcher.dispatch,
+ new sharedActions.CancelCall({}));
+ });
+
it("should dispatch a connection progress action on 'alerting'", function() {
store._websocket.trigger("progress", {state: WS_STATES.ALERTING});