mirror of
https://gitlab.winehq.org/wine/wine-gecko.git
synced 2024-09-13 09:24:08 -07:00
Bug 1000237 Standalone UI for link clickers needs "call being processed" visual notification. r=nperriault
This commit is contained in:
parent
082dcacd46
commit
a00a84e026
@ -135,6 +135,7 @@ p {
|
||||
background-color: #f0ad4e;
|
||||
}
|
||||
|
||||
.btn-cancel,
|
||||
.btn-error,
|
||||
.btn-hangup,
|
||||
.btn-error + .btn-chevron {
|
||||
@ -142,6 +143,7 @@ p {
|
||||
border: 1px solid #d74345;
|
||||
}
|
||||
|
||||
.btn-cancel:hover,
|
||||
.btn-error:hover,
|
||||
.btn-hangup:hover,
|
||||
.btn-error + .btn-chevron:hover {
|
||||
@ -149,6 +151,7 @@ p {
|
||||
border: 1px solid #c53436;
|
||||
}
|
||||
|
||||
.btn-cancel:active,
|
||||
.btn-error:active,
|
||||
.btn-hangup:active,
|
||||
.btn-error + .btn-chevron:active {
|
||||
|
@ -51,18 +51,6 @@ loop.shared.models = (function(l10n) {
|
||||
*/
|
||||
session: undefined,
|
||||
|
||||
/**
|
||||
* Pending call timeout value.
|
||||
* @type {Number}
|
||||
*/
|
||||
pendingCallTimeout: undefined,
|
||||
|
||||
/**
|
||||
* Pending call timer.
|
||||
* @type {Number}
|
||||
*/
|
||||
_pendingCallTimer: undefined,
|
||||
|
||||
/**
|
||||
* Constructor.
|
||||
*
|
||||
@ -71,10 +59,6 @@ loop.shared.models = (function(l10n) {
|
||||
* Required:
|
||||
* - {OT} sdk: OT SDK object.
|
||||
*
|
||||
* Optional:
|
||||
* - {Number} pendingCallTimeout: Pending call timeout in milliseconds
|
||||
* (default: 20000).
|
||||
*
|
||||
* @param {Object} attributes Attributes object.
|
||||
* @param {Object} options Options object.
|
||||
*/
|
||||
@ -84,10 +68,6 @@ loop.shared.models = (function(l10n) {
|
||||
throw new Error("missing required sdk");
|
||||
}
|
||||
this.sdk = options.sdk;
|
||||
this.pendingCallTimeout = options.pendingCallTimeout || 20000;
|
||||
|
||||
// Ensure that any pending call timer is cleared on disconnect/error
|
||||
this.on("session:ended session:error", this._clearPendingCallTimer, this);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -112,20 +92,6 @@ loop.shared.models = (function(l10n) {
|
||||
* server for the outgoing call.
|
||||
*/
|
||||
outgoing: function(sessionData) {
|
||||
this._clearPendingCallTimer();
|
||||
|
||||
// Outgoing call has never reached destination, closing - see bug 1020448
|
||||
function handleOutgoingCallTimeout() {
|
||||
/*jshint validthis:true */
|
||||
if (!this.get("ongoing")) {
|
||||
this.trigger("timeout").endSession();
|
||||
}
|
||||
}
|
||||
|
||||
// Setup pending call timeout.
|
||||
this._pendingCallTimer = setTimeout(
|
||||
handleOutgoingCallTimeout.bind(this), this.pendingCallTimeout);
|
||||
|
||||
this.setOutgoingSessionData(sessionData);
|
||||
this.trigger("call:outgoing");
|
||||
},
|
||||
@ -278,15 +244,6 @@ loop.shared.models = (function(l10n) {
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clears current pending call timer, if any.
|
||||
*/
|
||||
_clearPendingCallTimer: function() {
|
||||
if (this._pendingCallTimer) {
|
||||
clearTimeout(this._pendingCallTimer);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Manages connection status
|
||||
* triggers apropriate event for connection error/success
|
||||
|
@ -148,6 +148,18 @@ loop.CallConnectionWebSocket = (function() {
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Notifies the server that the outgoing call is cancelled by the
|
||||
* user.
|
||||
*/
|
||||
cancel: function() {
|
||||
this._send({
|
||||
messageType: "action",
|
||||
event: "terminate",
|
||||
reason: "cancel"
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Sends data on the websocket.
|
||||
*
|
||||
@ -206,6 +218,7 @@ loop.CallConnectionWebSocket = (function() {
|
||||
this._completeConnection();
|
||||
break;
|
||||
case "progress":
|
||||
this.trigger("progress:" + msg.state);
|
||||
this.trigger("progress", msg);
|
||||
break;
|
||||
}
|
||||
|
@ -3,7 +3,6 @@
|
||||
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
|
||||
LOOP_PENDING_CALL_TIMEOUT := $(shell echo $${LOOP_PENDING_CALL_TIMEOUT-20000})
|
||||
NODE_LOCAL_BIN=./node_modules/.bin
|
||||
|
||||
install:
|
||||
@ -53,4 +52,3 @@ config:
|
||||
@echo "var loop = loop || {};" > content/config.js
|
||||
@echo "loop.config = loop.config || {};" >> content/config.js
|
||||
@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
|
||||
@echo "loop.config.pendingCallTimeout = `echo $(LOOP_PENDING_CALL_TIMEOUT)`;" >> content/config.js
|
||||
|
@ -21,9 +21,12 @@ body,
|
||||
.standalone-header {
|
||||
border-radius: 4px;
|
||||
background: #fff;
|
||||
padding: 1rem 5rem;
|
||||
border: 1px solid #E7E7E7;
|
||||
box-shadow: 0px 2px 0px rgba(0, 0, 0, 0.03);
|
||||
}
|
||||
|
||||
.header-box {
|
||||
padding: 1rem 5rem;
|
||||
margin-top: 2rem;
|
||||
}
|
||||
|
||||
@ -103,7 +106,7 @@ body,
|
||||
}
|
||||
|
||||
.standalone-header-title,
|
||||
.standalone-call-btn-label {
|
||||
.standalone-btn-label {
|
||||
font-weight: lighter;
|
||||
}
|
||||
|
||||
@ -112,7 +115,7 @@ body,
|
||||
line-height: 2.2rem;
|
||||
}
|
||||
|
||||
.standalone-call-btn-label {
|
||||
.standalone-btn-label {
|
||||
font-size: 1.2rem;
|
||||
}
|
||||
|
||||
@ -179,6 +182,10 @@ body,
|
||||
}
|
||||
}
|
||||
|
||||
.btn-pending-cancel-group > .btn-cancel {
|
||||
flex: 2 1 auto;
|
||||
}
|
||||
|
||||
.btn-large {
|
||||
/* Dimensions from spec
|
||||
* https://people.mozilla.org/~dhenein/labs/loop-link-spec/#call-start */
|
||||
|
@ -120,6 +120,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationBranding = React.createClass({displayName: 'ConversationBranding',
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.h1({className: "standalone-header-title"},
|
||||
React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationHeader = React.createClass({displayName: 'ConversationHeader',
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
@ -138,10 +148,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
React.DOM.header({className: "standalone-header container-box"},
|
||||
React.DOM.h1({className: "standalone-header-title"},
|
||||
React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname")
|
||||
),
|
||||
React.DOM.header({className: "standalone-header header-box container-box"},
|
||||
ConversationBranding(null),
|
||||
React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}),
|
||||
React.DOM.h3({className: "call-url"},
|
||||
conversationUrl
|
||||
@ -165,6 +173,68 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
var PendingConversationView = React.createClass({displayName: 'PendingConversationView',
|
||||
getInitialState: function() {
|
||||
return {
|
||||
callState: this.props.callState || "connecting"
|
||||
}
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
|
||||
.isRequired
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
|
||||
this._handleRingingProgress);
|
||||
},
|
||||
|
||||
_handleRingingProgress: function() {
|
||||
this.setState({callState: "ringing"});
|
||||
},
|
||||
|
||||
_cancelOutgoingCall: function() {
|
||||
this.props.websocket.cancel();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
React.DOM.div({className: "container"},
|
||||
React.DOM.div({className: "container-box"},
|
||||
React.DOM.header({className: "pending-header header-box"},
|
||||
ConversationBranding(null)
|
||||
),
|
||||
|
||||
React.DOM.div({id: "cameraPreview"}),
|
||||
|
||||
React.DOM.div({id: "messages"}),
|
||||
|
||||
React.DOM.p({className: "standalone-btn-label"},
|
||||
callState
|
||||
),
|
||||
|
||||
React.DOM.div({className: "btn-pending-cancel-group btn-group"},
|
||||
React.DOM.div({className: "flex-padding-1"}),
|
||||
React.DOM.button({className: "btn btn-large btn-cancel",
|
||||
onClick: this._cancelOutgoingCall},
|
||||
React.DOM.span({className: "standalone-call-btn-text"},
|
||||
mozL10n.get("initiate_call_cancel_button")
|
||||
)
|
||||
),
|
||||
React.DOM.div({className: "flex-padding-1"})
|
||||
)
|
||||
),
|
||||
|
||||
ConversationFooter(null)
|
||||
)
|
||||
/* jshint ignore:end */
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation launcher view. A ConversationModel is associated and attached
|
||||
* as a `model` property.
|
||||
@ -286,7 +356,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
ConversationHeader({
|
||||
urlCreationDateString: this.state.urlCreationDateString}),
|
||||
|
||||
React.DOM.p({className: "standalone-call-btn-label"},
|
||||
React.DOM.p({className: "standalone-btn-label"},
|
||||
mozL10n.get("initiate_call_button_label2")
|
||||
),
|
||||
|
||||
@ -352,6 +422,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
"unsupportedDevice": "unsupportedDevice",
|
||||
"unsupportedBrowser": "unsupportedBrowser",
|
||||
"call/expired": "expired",
|
||||
"call/pending/:token": "pendingConversation",
|
||||
"call/ongoing/:token": "loadConversation",
|
||||
"call/:token": "initiate"
|
||||
},
|
||||
@ -364,8 +435,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
|
||||
// Load default view
|
||||
this.loadReactComponent(HomeView(null));
|
||||
|
||||
this.listenTo(this._conversation, "timeout", this._onTimeout);
|
||||
},
|
||||
|
||||
_onSessionExpired: function() {
|
||||
@ -417,7 +486,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
this._notifications.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this._setupWebSocketAndCallView(loopToken);
|
||||
this.navigate("call/pending/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -427,16 +498,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
*
|
||||
* @param {string} loopToken The session token to use.
|
||||
*/
|
||||
_setupWebSocketAndCallView: function(loopToken) {
|
||||
_setupWebSocketAndCallView: function() {
|
||||
this._websocket = new loop.CallConnectionWebSocket({
|
||||
url: this._conversation.get("progressURL"),
|
||||
websocketToken: this._conversation.get("websocketToken"),
|
||||
callId: this._conversation.get("callId"),
|
||||
});
|
||||
this._websocket.promiseConnect().then(function() {
|
||||
this.navigate("call/ongoing/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
}.bind(this), function() {
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
// this by better "call failed" UI.
|
||||
@ -464,30 +532,48 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
* it if appropraite.
|
||||
*/
|
||||
_handleWebSocketProgress: function(progressData) {
|
||||
if (progressData.state === "terminated") {
|
||||
// XXX Before adding more states here, the basic protocol messages to the
|
||||
// server need implementing on both the standalone and desktop side.
|
||||
// These are covered by bug 1045643, but also check the dependencies on
|
||||
// bug 1034041.
|
||||
//
|
||||
// Failure to do this will break desktop - standalone call setup. We're
|
||||
// ok to handle reject, as that is a specific message from the destkop via
|
||||
// the server.
|
||||
switch (progressData.reason) {
|
||||
case "reject":
|
||||
this._handleCallRejected();
|
||||
switch(progressData.state) {
|
||||
case "connecting": {
|
||||
this._handleCallConnecting();
|
||||
break;
|
||||
}
|
||||
case "terminated": {
|
||||
// At the moment, we show the same text regardless
|
||||
// of the terminated reason.
|
||||
this._handleCallTerminated(progressData.reason);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles call rejection.
|
||||
* XXX This should really display the call failed view - bug 1046959
|
||||
* will implement this.
|
||||
* Handles a call moving to the connecting stage.
|
||||
*/
|
||||
_handleCallRejected: function() {
|
||||
_handleCallConnecting: function() {
|
||||
var loopToken = this._conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this._notifications.errorL10n("missing_conversation_info");
|
||||
return;
|
||||
}
|
||||
|
||||
this.navigate("call/ongoing/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles call rejection.
|
||||
*
|
||||
* @param {String} reason The reason the call was terminated.
|
||||
*/
|
||||
_handleCallTerminated: function(reason) {
|
||||
this.endCall();
|
||||
this._notifications.errorL10n("call_timeout_notification_text");
|
||||
// For reasons other than cancel, display some notification text.
|
||||
if (reason !== "cancel") {
|
||||
// XXX This should really display the call failed view - bug 1046959
|
||||
// will implement this.
|
||||
this._notifications.errorL10n("call_timeout_notification_text");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -501,10 +587,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
this.navigate(route, {trigger: true});
|
||||
},
|
||||
|
||||
_onTimeout: function() {
|
||||
this._notifications.errorL10n("call_timeout_notification_text");
|
||||
},
|
||||
|
||||
/**
|
||||
* Default entry point.
|
||||
*/
|
||||
@ -549,6 +631,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
this.loadReactComponent(startView);
|
||||
},
|
||||
|
||||
pendingConversation: function(loopToken) {
|
||||
if (!this._conversation.isSessionReady()) {
|
||||
// User has loaded this url directly, actually setup the call.
|
||||
return this.navigate("call/" + loopToken, {trigger: true});
|
||||
}
|
||||
this._setupWebSocketAndCallView();
|
||||
this.loadReactComponent(PendingConversationView({
|
||||
websocket: this._websocket
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads conversation establishment view.
|
||||
*
|
||||
@ -596,8 +689,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
notifications: new sharedModels.NotificationCollection(),
|
||||
client: client,
|
||||
conversation: new sharedModels.ConversationModel({}, {
|
||||
sdk: OT,
|
||||
pendingCallTimeout: loop.config.pendingCallTimeout
|
||||
sdk: OT
|
||||
})
|
||||
});
|
||||
|
||||
@ -616,6 +708,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
return {
|
||||
baseServerUrl: baseServerUrl,
|
||||
CallUrlExpiredView: CallUrlExpiredView,
|
||||
PendingConversationView: PendingConversationView,
|
||||
StartConversationView: StartConversationView,
|
||||
HomeView: HomeView,
|
||||
UnsupportedBrowserView: UnsupportedBrowserView,
|
||||
|
@ -120,6 +120,16 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationBranding = React.createClass({
|
||||
render: function() {
|
||||
return (
|
||||
<h1 className="standalone-header-title">
|
||||
<strong>{mozL10n.get("brandShortname")}</strong> {mozL10n.get("clientShortname")}
|
||||
</h1>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
var ConversationHeader = React.createClass({
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
@ -138,10 +148,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
<header className="standalone-header container-box">
|
||||
<h1 className="standalone-header-title">
|
||||
<strong>{mozL10n.get("brandShortname")}</strong> {mozL10n.get("clientShortname")}
|
||||
</h1>
|
||||
<header className="standalone-header header-box container-box">
|
||||
<ConversationBranding />
|
||||
<div className="loop-logo" title="Firefox WebRTC! logo"></div>
|
||||
<h3 className="call-url">
|
||||
{conversationUrl}
|
||||
@ -165,6 +173,68 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
var PendingConversationView = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {
|
||||
callState: this.props.callState || "connecting"
|
||||
}
|
||||
},
|
||||
|
||||
propTypes: {
|
||||
websocket: React.PropTypes.instanceOf(loop.CallConnectionWebSocket)
|
||||
.isRequired
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this.props.websocket.listenTo(this.props.websocket, "progress:alerting",
|
||||
this._handleRingingProgress);
|
||||
},
|
||||
|
||||
_handleRingingProgress: function() {
|
||||
this.setState({callState: "ringing"});
|
||||
},
|
||||
|
||||
_cancelOutgoingCall: function() {
|
||||
this.props.websocket.cancel();
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
<div className="container">
|
||||
<div className="container-box">
|
||||
<header className="pending-header header-box">
|
||||
<ConversationBranding />
|
||||
</header>
|
||||
|
||||
<div id="cameraPreview"></div>
|
||||
|
||||
<div id="messages"></div>
|
||||
|
||||
<p className="standalone-btn-label">
|
||||
{callState}
|
||||
</p>
|
||||
|
||||
<div className="btn-pending-cancel-group btn-group">
|
||||
<div className="flex-padding-1"></div>
|
||||
<button className="btn btn-large btn-cancel"
|
||||
onClick={this._cancelOutgoingCall} >
|
||||
<span className="standalone-call-btn-text">
|
||||
{mozL10n.get("initiate_call_cancel_button")}
|
||||
</span>
|
||||
</button>
|
||||
<div className="flex-padding-1"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ConversationFooter />
|
||||
</div>
|
||||
/* jshint ignore:end */
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation launcher view. A ConversationModel is associated and attached
|
||||
* as a `model` property.
|
||||
@ -286,7 +356,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
<ConversationHeader
|
||||
urlCreationDateString={this.state.urlCreationDateString} />
|
||||
|
||||
<p className="standalone-call-btn-label">
|
||||
<p className="standalone-btn-label">
|
||||
{mozL10n.get("initiate_call_button_label2")}
|
||||
</p>
|
||||
|
||||
@ -352,6 +422,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
"unsupportedDevice": "unsupportedDevice",
|
||||
"unsupportedBrowser": "unsupportedBrowser",
|
||||
"call/expired": "expired",
|
||||
"call/pending/:token": "pendingConversation",
|
||||
"call/ongoing/:token": "loadConversation",
|
||||
"call/:token": "initiate"
|
||||
},
|
||||
@ -364,8 +435,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
|
||||
// Load default view
|
||||
this.loadReactComponent(<HomeView />);
|
||||
|
||||
this.listenTo(this._conversation, "timeout", this._onTimeout);
|
||||
},
|
||||
|
||||
_onSessionExpired: function() {
|
||||
@ -417,7 +486,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
this._notifications.errorL10n("missing_conversation_info");
|
||||
this.navigate("home", {trigger: true});
|
||||
} else {
|
||||
this._setupWebSocketAndCallView(loopToken);
|
||||
this.navigate("call/pending/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
@ -427,16 +498,13 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
*
|
||||
* @param {string} loopToken The session token to use.
|
||||
*/
|
||||
_setupWebSocketAndCallView: function(loopToken) {
|
||||
_setupWebSocketAndCallView: function() {
|
||||
this._websocket = new loop.CallConnectionWebSocket({
|
||||
url: this._conversation.get("progressURL"),
|
||||
websocketToken: this._conversation.get("websocketToken"),
|
||||
callId: this._conversation.get("callId"),
|
||||
});
|
||||
this._websocket.promiseConnect().then(function() {
|
||||
this.navigate("call/ongoing/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
}.bind(this), function() {
|
||||
// XXX Not the ideal response, but bug 1047410 will be replacing
|
||||
// this by better "call failed" UI.
|
||||
@ -464,30 +532,48 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
* it if appropraite.
|
||||
*/
|
||||
_handleWebSocketProgress: function(progressData) {
|
||||
if (progressData.state === "terminated") {
|
||||
// XXX Before adding more states here, the basic protocol messages to the
|
||||
// server need implementing on both the standalone and desktop side.
|
||||
// These are covered by bug 1045643, but also check the dependencies on
|
||||
// bug 1034041.
|
||||
//
|
||||
// Failure to do this will break desktop - standalone call setup. We're
|
||||
// ok to handle reject, as that is a specific message from the destkop via
|
||||
// the server.
|
||||
switch (progressData.reason) {
|
||||
case "reject":
|
||||
this._handleCallRejected();
|
||||
switch(progressData.state) {
|
||||
case "connecting": {
|
||||
this._handleCallConnecting();
|
||||
break;
|
||||
}
|
||||
case "terminated": {
|
||||
// At the moment, we show the same text regardless
|
||||
// of the terminated reason.
|
||||
this._handleCallTerminated(progressData.reason);
|
||||
break;
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles call rejection.
|
||||
* XXX This should really display the call failed view - bug 1046959
|
||||
* will implement this.
|
||||
* Handles a call moving to the connecting stage.
|
||||
*/
|
||||
_handleCallRejected: function() {
|
||||
_handleCallConnecting: function() {
|
||||
var loopToken = this._conversation.get("loopToken");
|
||||
if (!loopToken) {
|
||||
this._notifications.errorL10n("missing_conversation_info");
|
||||
return;
|
||||
}
|
||||
|
||||
this.navigate("call/ongoing/" + loopToken, {
|
||||
trigger: true
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles call rejection.
|
||||
*
|
||||
* @param {String} reason The reason the call was terminated.
|
||||
*/
|
||||
_handleCallTerminated: function(reason) {
|
||||
this.endCall();
|
||||
this._notifications.errorL10n("call_timeout_notification_text");
|
||||
// For reasons other than cancel, display some notification text.
|
||||
if (reason !== "cancel") {
|
||||
// XXX This should really display the call failed view - bug 1046959
|
||||
// will implement this.
|
||||
this._notifications.errorL10n("call_timeout_notification_text");
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
@ -501,10 +587,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
this.navigate(route, {trigger: true});
|
||||
},
|
||||
|
||||
_onTimeout: function() {
|
||||
this._notifications.errorL10n("call_timeout_notification_text");
|
||||
},
|
||||
|
||||
/**
|
||||
* Default entry point.
|
||||
*/
|
||||
@ -549,6 +631,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
this.loadReactComponent(startView);
|
||||
},
|
||||
|
||||
pendingConversation: function(loopToken) {
|
||||
if (!this._conversation.isSessionReady()) {
|
||||
// User has loaded this url directly, actually setup the call.
|
||||
return this.navigate("call/" + loopToken, {trigger: true});
|
||||
}
|
||||
this._setupWebSocketAndCallView();
|
||||
this.loadReactComponent(PendingConversationView({
|
||||
websocket: this._websocket
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Loads conversation establishment view.
|
||||
*
|
||||
@ -596,8 +689,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
notifications: new sharedModels.NotificationCollection(),
|
||||
client: client,
|
||||
conversation: new sharedModels.ConversationModel({}, {
|
||||
sdk: OT,
|
||||
pendingCallTimeout: loop.config.pendingCallTimeout
|
||||
sdk: OT
|
||||
})
|
||||
});
|
||||
|
||||
@ -616,6 +708,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||
return {
|
||||
baseServerUrl: baseServerUrl,
|
||||
CallUrlExpiredView: CallUrlExpiredView,
|
||||
PendingConversationView: PendingConversationView,
|
||||
StartConversationView: StartConversationView,
|
||||
HomeView: HomeView,
|
||||
UnsupportedBrowserView: UnsupportedBrowserView,
|
||||
|
@ -36,7 +36,7 @@ initiate_call_button_label2=Ready to start your conversation?
|
||||
initiate_audio_video_call_button2=Start
|
||||
initiate_audio_video_call_tooltip2=Start a video conversation
|
||||
initiate_audio_call_button2=Voice conversation
|
||||
reject_incoming_call=Cancel
|
||||
initiate_call_cancel_button=Cancel
|
||||
legal_text_and_links=By using this product you agree to the {{terms_of_use_url}} and {{privacy_notice_url}}
|
||||
terms_of_use_link_text=Terms of use
|
||||
privacy_notice_link_text=Privacy notice
|
||||
|
@ -15,8 +15,7 @@ app.get('/content/config.js', function (req, res) {
|
||||
res.send(
|
||||
"var loop = loop || {};" +
|
||||
"loop.config = loop.config || {};" +
|
||||
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" +
|
||||
"loop.config.pendingCallTimeout = 20000;"
|
||||
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';"
|
||||
);
|
||||
|
||||
});
|
||||
|
@ -53,13 +53,6 @@ describe("loop.shared.models", function() {
|
||||
new sharedModels.ConversationModel({}, {});
|
||||
}).to.Throw(Error, /missing required sdk/);
|
||||
});
|
||||
|
||||
it("should accept a pendingCallTimeout option", function() {
|
||||
expect(new sharedModels.ConversationModel({}, {
|
||||
sdk: {},
|
||||
pendingCallTimeout: 1000
|
||||
}).pendingCallTimeout).eql(1000);
|
||||
});
|
||||
});
|
||||
|
||||
describe("constructed", function() {
|
||||
@ -68,8 +61,7 @@ describe("loop.shared.models", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
conversation = new sharedModels.ConversationModel({}, {
|
||||
sdk: fakeSDK,
|
||||
pendingCallTimeout: 1000
|
||||
sdk: fakeSDK
|
||||
});
|
||||
conversation.set("loopToken", "fakeToken");
|
||||
fakeBaseServerUrl = "http://fakeBaseServerUrl";
|
||||
@ -121,25 +113,6 @@ describe("loop.shared.models", function() {
|
||||
|
||||
conversation.outgoing();
|
||||
});
|
||||
|
||||
it("should end the session on outgoing call timeout", function() {
|
||||
conversation.outgoing();
|
||||
|
||||
sandbox.clock.tick(1001);
|
||||
|
||||
sinon.assert.calledOnce(conversation.endSession);
|
||||
});
|
||||
|
||||
it("should trigger a `timeout` event on outgoing call timeout",
|
||||
function(done) {
|
||||
conversation.once("timeout", function() {
|
||||
done();
|
||||
});
|
||||
|
||||
conversation.outgoing();
|
||||
|
||||
sandbox.clock.tick(1001);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setSessionData", function() {
|
||||
@ -168,11 +141,8 @@ describe("loop.shared.models", function() {
|
||||
var model;
|
||||
|
||||
beforeEach(function() {
|
||||
sandbox.stub(sharedModels.ConversationModel.prototype,
|
||||
"_clearPendingCallTimer");
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK,
|
||||
pendingCallTimeout: 1000
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
@ -281,18 +251,6 @@ describe("loop.shared.models", function() {
|
||||
expect(model.get("ongoing")).eql(false);
|
||||
});
|
||||
|
||||
it("should clear a pending timer on session:ended", function() {
|
||||
model.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(model._clearPendingCallTimer);
|
||||
});
|
||||
|
||||
it("should clear a pending timer on session:error", function() {
|
||||
model.trigger("session:error");
|
||||
|
||||
sinon.assert.calledOnce(model._clearPendingCallTimer);
|
||||
});
|
||||
|
||||
describe("connectionDestroyed event received", function() {
|
||||
var fakeEvent = {reason: "ko", connection: {connectionId: 42}};
|
||||
|
||||
@ -341,8 +299,7 @@ describe("loop.shared.models", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK,
|
||||
pendingCallTimeout: 1000
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
@ -381,8 +338,7 @@ describe("loop.shared.models", function() {
|
||||
|
||||
beforeEach(function() {
|
||||
model = new sharedModels.ConversationModel(fakeSessionData, {
|
||||
sdk: fakeSDK,
|
||||
pendingCallTimeout: 1000
|
||||
sdk: fakeSDK
|
||||
});
|
||||
model.startSession();
|
||||
});
|
||||
|
@ -176,6 +176,22 @@ describe("loop.CallConnectionWebSocket", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#cancel", function() {
|
||||
it("should send a terminate message to the server with a reason of cancel",
|
||||
function() {
|
||||
callWebSocket.promiseConnect();
|
||||
|
||||
callWebSocket.cancel();
|
||||
|
||||
sinon.assert.calledOnce(dummySocket.send);
|
||||
sinon.assert.calledWithExactly(dummySocket.send, JSON.stringify({
|
||||
messageType: "action",
|
||||
event: "terminate",
|
||||
reason: "cancel"
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(callWebSocket, "trigger");
|
||||
@ -195,9 +211,24 @@ describe("loop.CallConnectionWebSocket", function() {
|
||||
data: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(callWebSocket.trigger);
|
||||
sinon.assert.called(callWebSocket.trigger);
|
||||
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress", eventData);
|
||||
});
|
||||
|
||||
it("should trigger a progress:<state> event on the callWebSocket", function() {
|
||||
var eventData = {
|
||||
messageType: "progress",
|
||||
state: "terminate",
|
||||
reason: "reject"
|
||||
};
|
||||
|
||||
dummySocket.onmessage({
|
||||
data: JSON.stringify(eventData)
|
||||
});
|
||||
|
||||
sinon.assert.called(callWebSocket.trigger);
|
||||
sinon.assert.calledWithExactly(callWebSocket.trigger, "progress:terminate");
|
||||
});
|
||||
});
|
||||
|
||||
describe("Error", function() {
|
||||
|
@ -88,6 +88,14 @@ describe("loop.webapp", function() {
|
||||
sandbox.stub(router, "navigate");
|
||||
});
|
||||
|
||||
describe("#initialize", function() {
|
||||
it("should require a conversation option", function() {
|
||||
expect(function() {
|
||||
new loop.webapp.WebappRouter();
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#startCall", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(router, "_setupWebSocketAndCallView");
|
||||
@ -109,14 +117,15 @@ describe("loop.webapp", function() {
|
||||
"missing_conversation_info");
|
||||
});
|
||||
|
||||
it("should setup the websocket if session token is available", function() {
|
||||
conversation.set("loopToken", "fake");
|
||||
it("should navigate to the pending view if session token is available",
|
||||
function() {
|
||||
conversation.set("loopToken", "fake");
|
||||
|
||||
router.startCall();
|
||||
router.startCall();
|
||||
|
||||
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
|
||||
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView, "fake");
|
||||
});
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/pending/fake");
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_setupWebSocketAndCallView", function() {
|
||||
@ -126,7 +135,7 @@ describe("loop.webapp", function() {
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey",
|
||||
callId: "Hello",
|
||||
progressURL: "http://progress.example.com",
|
||||
progressURL: "http://invalid/url",
|
||||
websocketToken: 123
|
||||
});
|
||||
});
|
||||
@ -154,23 +163,13 @@ describe("loop.webapp", function() {
|
||||
sinon.assert.calledOnce(loop.CallConnectionWebSocket);
|
||||
sinon.assert.calledWithExactly(loop.CallConnectionWebSocket, {
|
||||
callId: "Hello",
|
||||
url: "http://progress.example.com",
|
||||
url: "http://invalid/url",
|
||||
// The websocket token is converted to a hex string.
|
||||
websocketToken: "7b"
|
||||
});
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should navigate to call/ongoing/:token", function(done) {
|
||||
router._setupWebSocketAndCallView("fake");
|
||||
|
||||
promise.then(function () {
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("Websocket connection failed", function() {
|
||||
@ -226,6 +225,7 @@ describe("loop.webapp", function() {
|
||||
describe("state: terminate, reason: reject", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(router, "endCall");
|
||||
sandbox.stub(notifications, "errorL10n");
|
||||
});
|
||||
|
||||
it("should end the call", function() {
|
||||
@ -237,17 +237,39 @@ describe("loop.webapp", function() {
|
||||
sinon.assert.calledOnce(router.endCall);
|
||||
});
|
||||
|
||||
it("should display an error message", function() {
|
||||
sandbox.stub(notifications, "errorL10n");
|
||||
it("should display an error message if the reason is not 'cancel'",
|
||||
function() {
|
||||
router._websocket.trigger("progress", {
|
||||
state: "terminated",
|
||||
reason: "reject"
|
||||
});
|
||||
|
||||
router._websocket.trigger("progress", {
|
||||
state: "terminated",
|
||||
reason: "reject"
|
||||
sinon.assert.calledOnce(notifications.errorL10n);
|
||||
sinon.assert.calledWithExactly(notifications.errorL10n,
|
||||
"call_timeout_notification_text");
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(router._notifications.errorL10n);
|
||||
sinon.assert.calledWithExactly(router._notifications.errorL10n,
|
||||
"call_timeout_notification_text");
|
||||
it("should not display an error message if the reason is 'cancel'",
|
||||
function() {
|
||||
router._websocket.trigger("progress", {
|
||||
state: "terminated",
|
||||
reason: "cancel"
|
||||
});
|
||||
|
||||
sinon.assert.notCalled(notifications.errorL10n);
|
||||
});
|
||||
});
|
||||
|
||||
describe("state: connecting", function() {
|
||||
it("should navigate to the ongoing view", function() {
|
||||
conversation.set({"loopToken": "fakeToken"});
|
||||
|
||||
router._websocket.trigger("progress", {
|
||||
state: "connecting"
|
||||
});
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWithMatch(router.navigate, "call/ongoing/fake");
|
||||
});
|
||||
});
|
||||
});
|
||||
@ -334,6 +356,38 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#pendingConversation", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(router, "_setupWebSocketAndCallView");
|
||||
conversation.setOutgoingSessionData({
|
||||
sessionId: "sessionId",
|
||||
sessionToken: "sessionToken",
|
||||
apiKey: "apiKey",
|
||||
callId: "Hello",
|
||||
progressURL: "http://progress.example.com",
|
||||
websocketToken: 123
|
||||
});
|
||||
});
|
||||
|
||||
it("should setup the websocket", function() {
|
||||
router.pendingConversation();
|
||||
|
||||
sinon.assert.calledOnce(router._setupWebSocketAndCallView);
|
||||
sinon.assert.calledWithExactly(router._setupWebSocketAndCallView);
|
||||
});
|
||||
|
||||
it("should load the PendingConversationView", function() {
|
||||
router.pendingConversation();
|
||||
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return React.addons.TestUtils.isDescriptorOfType(
|
||||
value, loop.webapp.PendingConversationView);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#loadConversation", function() {
|
||||
it("should load the ConversationView if session is set", function() {
|
||||
conversation.set("sessionId", "fakeSessionId");
|
||||
@ -548,15 +602,46 @@ describe("loop.webapp", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("StartConversationView", function() {
|
||||
describe("#initialize", function() {
|
||||
it("should require a conversation option", function() {
|
||||
expect(function() {
|
||||
new loop.webapp.WebappRouter();
|
||||
}).to.Throw(Error, /missing required conversation/);
|
||||
describe("PendingConversationView", function() {
|
||||
var view, websocket;
|
||||
|
||||
beforeEach(function() {
|
||||
websocket = new loop.CallConnectionWebSocket({
|
||||
url: "wss://fake/",
|
||||
callId: "callId",
|
||||
websocketToken: "7b"
|
||||
});
|
||||
|
||||
sinon.stub(websocket, "cancel");
|
||||
|
||||
view = React.addons.TestUtils.renderIntoDocument(
|
||||
loop.webapp.PendingConversationView({
|
||||
websocket: websocket
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
describe("#_cancelOutgoingCall", function() {
|
||||
it("should inform the websocket to cancel the setup", function() {
|
||||
var button = view.getDOMNode().querySelector(".btn-cancel");
|
||||
React.addons.TestUtils.Simulate.click(button);
|
||||
|
||||
sinon.assert.calledOnce(websocket.cancel);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Events", function() {
|
||||
describe("progress:alerting", function() {
|
||||
it("should update the callstate to ringing", function () {
|
||||
websocket.trigger("progress:alerting");
|
||||
|
||||
expect(view.state.callState).to.be.equal("ringing");
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("StartConversationView", function() {
|
||||
describe("#initiate", function() {
|
||||
var conversation, setupOutgoingCall, view, fakeSubmitEvent,
|
||||
requestCallUrlInfo;
|
||||
|
@ -21,6 +21,7 @@
|
||||
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
|
||||
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
|
||||
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
|
||||
var PendingConversationView = loop.webapp.PendingConversationView;
|
||||
var StartConversationView = loop.webapp.StartConversationView;
|
||||
|
||||
// 3. Shared components
|
||||
@ -195,6 +196,19 @@
|
||||
)
|
||||
),
|
||||
|
||||
Section({name: "PendingConversationView"},
|
||||
Example({summary: "Pending conversation view (connecting)", dashed: "true"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
PendingConversationView(null)
|
||||
)
|
||||
),
|
||||
Example({summary: "Pending conversation view (ringing)", dashed: "true"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
PendingConversationView({callState: "ringing"})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
Section({name: "StartConversationView"},
|
||||
Example({summary: "Start conversation view", dashed: "true"},
|
||||
React.DOM.div({className: "standalone"},
|
||||
|
@ -21,6 +21,7 @@
|
||||
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
|
||||
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
|
||||
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
|
||||
var PendingConversationView = loop.webapp.PendingConversationView;
|
||||
var StartConversationView = loop.webapp.StartConversationView;
|
||||
|
||||
// 3. Shared components
|
||||
@ -195,6 +196,19 @@
|
||||
</div>
|
||||
</Section>
|
||||
|
||||
<Section name="PendingConversationView">
|
||||
<Example summary="Pending conversation view (connecting)" dashed="true">
|
||||
<div className="standalone">
|
||||
<PendingConversationView />
|
||||
</div>
|
||||
</Example>
|
||||
<Example summary="Pending conversation view (ringing)" dashed="true">
|
||||
<div className="standalone">
|
||||
<PendingConversationView callState="ringing"/>
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="StartConversationView">
|
||||
<Example summary="Start conversation view" dashed="true">
|
||||
<div className="standalone">
|
||||
|
Loading…
Reference in New Issue
Block a user