Bug 1000240 - Add a Call Failed view for Loop standalone UI. r=Standard8

This commit is contained in:
Nicolas Perriault
2014-09-26 23:10:08 +01:00
parent 72d567f3b1
commit b0672f4958
13 changed files with 434 additions and 384 deletions

View File

@@ -11,10 +11,12 @@ var loop = loop || {};
loop.conversation = (function(mozL10n) { loop.conversation = (function(mozL10n) {
"use strict"; "use strict";
var sharedViews = loop.shared.views, var sharedViews = loop.shared.views;
sharedModels = loop.shared.models; var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var IncomingCallView = React.createClass({displayName: 'IncomingCallView', var IncomingCallView = React.createClass({displayName: 'IncomingCallView',
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: { propTypes: {
model: React.PropTypes.object.isRequired, model: React.PropTypes.object.isRequired,
@@ -23,25 +25,11 @@ loop.conversation = (function(mozL10n) {
getDefaultProps: function() { getDefaultProps: function() {
return { return {
showDeclineMenu: false, showMenu: false,
video: true video: true
}; };
}, },
getInitialState: function() {
return {showDeclineMenu: this.props.showDeclineMenu};
},
componentDidMount: function() {
window.addEventListener("click", this.clickHandler);
window.addEventListener("blur", this._hideDeclineMenu);
},
componentWillUnmount: function() {
window.removeEventListener("click", this.clickHandler);
window.removeEventListener("blur", this._hideDeclineMenu);
},
clickHandler: function(e) { clickHandler: function(e) {
var target = e.target; var target = e.target;
if (!target.classList.contains('btn-chevron')) { if (!target.classList.contains('btn-chevron')) {
@@ -67,15 +55,6 @@ loop.conversation = (function(mozL10n) {
return false; return false;
}, },
_toggleDeclineMenu: function() {
var currentState = this.state.showDeclineMenu;
this.setState({showDeclineMenu: !currentState});
},
_hideDeclineMenu: function() {
this.setState({showDeclineMenu: false});
},
/* /*
* Generate props for <AcceptCallButton> component based on * Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video * incoming call type. An incoming video call will render a video
@@ -109,16 +88,13 @@ loop.conversation = (function(mozL10n) {
render: function() { render: function() {
/* jshint ignore:start */ /* jshint ignore:start */
var btnClassAccept = "btn btn-accept";
var btnClassDecline = "btn btn-error btn-decline";
var conversationPanelClass = "incoming-call";
var dropdownMenuClassesDecline = React.addons.classSet({ var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true, "native-dropdown-menu": true,
"conversation-window-dropdown": true, "conversation-window-dropdown": true,
"visually-hidden": !this.state.showDeclineMenu "visually-hidden": !this.state.showMenu
}); });
return ( return (
React.DOM.div({className: conversationPanelClass}, React.DOM.div({className: "incoming-call"},
React.DOM.h2(null, mozL10n.get("incoming_call_title2")), React.DOM.h2(null, mozL10n.get("incoming_call_title2")),
React.DOM.div({className: "btn-group incoming-call-action-group"}, React.DOM.div({className: "btn-group incoming-call-action-group"},
@@ -128,13 +104,11 @@ loop.conversation = (function(mozL10n) {
React.DOM.div({className: "btn-group-chevron"}, React.DOM.div({className: "btn-group-chevron"},
React.DOM.div({className: "btn-group"}, React.DOM.div({className: "btn-group"},
React.DOM.button({className: btnClassDecline, React.DOM.button({className: "btn btn-decline",
onClick: this._handleDecline}, onClick: this._handleDecline},
mozL10n.get("incoming_call_cancel_button") mozL10n.get("incoming_call_cancel_button")
), ),
React.DOM.div({className: "btn-chevron", React.DOM.div({className: "btn-chevron", onClick: this.toggleDropdownMenu})
onClick: this._toggleDeclineMenu}
)
), ),
React.DOM.ul({className: dropdownMenuClassesDecline}, React.DOM.ul({className: dropdownMenuClassesDecline},

View File

@@ -11,10 +11,12 @@ var loop = loop || {};
loop.conversation = (function(mozL10n) { loop.conversation = (function(mozL10n) {
"use strict"; "use strict";
var sharedViews = loop.shared.views, var sharedViews = loop.shared.views;
sharedModels = loop.shared.models; var sharedMixins = loop.shared.mixins;
var sharedModels = loop.shared.models;
var IncomingCallView = React.createClass({ var IncomingCallView = React.createClass({
mixins: [sharedMixins.DropdownMenuMixin],
propTypes: { propTypes: {
model: React.PropTypes.object.isRequired, model: React.PropTypes.object.isRequired,
@@ -23,25 +25,11 @@ loop.conversation = (function(mozL10n) {
getDefaultProps: function() { getDefaultProps: function() {
return { return {
showDeclineMenu: false, showMenu: false,
video: true video: true
}; };
}, },
getInitialState: function() {
return {showDeclineMenu: this.props.showDeclineMenu};
},
componentDidMount: function() {
window.addEventListener("click", this.clickHandler);
window.addEventListener("blur", this._hideDeclineMenu);
},
componentWillUnmount: function() {
window.removeEventListener("click", this.clickHandler);
window.removeEventListener("blur", this._hideDeclineMenu);
},
clickHandler: function(e) { clickHandler: function(e) {
var target = e.target; var target = e.target;
if (!target.classList.contains('btn-chevron')) { if (!target.classList.contains('btn-chevron')) {
@@ -67,15 +55,6 @@ loop.conversation = (function(mozL10n) {
return false; return false;
}, },
_toggleDeclineMenu: function() {
var currentState = this.state.showDeclineMenu;
this.setState({showDeclineMenu: !currentState});
},
_hideDeclineMenu: function() {
this.setState({showDeclineMenu: false});
},
/* /*
* Generate props for <AcceptCallButton> component based on * Generate props for <AcceptCallButton> component based on
* incoming call type. An incoming video call will render a video * incoming call type. An incoming video call will render a video
@@ -109,16 +88,13 @@ loop.conversation = (function(mozL10n) {
render: function() { render: function() {
/* jshint ignore:start */ /* jshint ignore:start */
var btnClassAccept = "btn btn-accept";
var btnClassDecline = "btn btn-error btn-decline";
var conversationPanelClass = "incoming-call";
var dropdownMenuClassesDecline = React.addons.classSet({ var dropdownMenuClassesDecline = React.addons.classSet({
"native-dropdown-menu": true, "native-dropdown-menu": true,
"conversation-window-dropdown": true, "conversation-window-dropdown": true,
"visually-hidden": !this.state.showDeclineMenu "visually-hidden": !this.state.showMenu
}); });
return ( return (
<div className={conversationPanelClass}> <div className="incoming-call">
<h2>{mozL10n.get("incoming_call_title2")}</h2> <h2>{mozL10n.get("incoming_call_title2")}</h2>
<div className="btn-group incoming-call-action-group"> <div className="btn-group incoming-call-action-group">
@@ -128,13 +104,11 @@ loop.conversation = (function(mozL10n) {
<div className="btn-group-chevron"> <div className="btn-group-chevron">
<div className="btn-group"> <div className="btn-group">
<button className={btnClassDecline} <button className="btn btn-decline"
onClick={this._handleDecline}> onClick={this._handleDecline}>
{mozL10n.get("incoming_call_cancel_button")} {mozL10n.get("incoming_call_cancel_button")}
</button> </button>
<div className="btn-chevron" <div className="btn-chevron" onClick={this.toggleDropdownMenu} />
onClick={this._toggleDeclineMenu}>
</div>
</div> </div>
<ul className={dropdownMenuClassesDecline}> <ul className={dropdownMenuClassesDecline}>

View File

@@ -137,7 +137,9 @@ p {
.btn-cancel, .btn-cancel,
.btn-error, .btn-error,
.btn-decline,
.btn-hangup, .btn-hangup,
.btn-decline + .btn-chevron,
.btn-error + .btn-chevron { .btn-error + .btn-chevron {
background-color: #d74345; background-color: #d74345;
border: 1px solid #d74345; border: 1px solid #d74345;
@@ -145,7 +147,9 @@ p {
.btn-cancel:hover, .btn-cancel:hover,
.btn-error:hover, .btn-error:hover,
.btn-decline:hover,
.btn-hangup:hover, .btn-hangup:hover,
.btn-decline + .btn-chevron:hover,
.btn-error + .btn-chevron:hover { .btn-error + .btn-chevron:hover {
background-color: #c53436; background-color: #c53436;
border: 1px solid #c53436; border: 1px solid #c53436;
@@ -153,7 +157,9 @@ p {
.btn-cancel:active, .btn-cancel:active,
.btn-error:active, .btn-error:active,
.btn-decline:active,
.btn-hangup:active, .btn-hangup:active,
.btn-decline + .btn-chevron:active,
.btn-error + .btn-chevron:active { .btn-error + .btn-chevron:active {
background-color: #ae2325; background-color: #ae2325;
border: 1px solid #ae2325; border: 1px solid #ae2325;
@@ -182,6 +188,7 @@ p {
} }
.btn-group-chevron .btn { .btn-group-chevron .btn {
border-radius: 2px;
border-top-right-radius: 0; border-top-right-radius: 0;
border-bottom-right-radius: 0; border-bottom-right-radius: 0;
flex: 2; flex: 2;
@@ -369,7 +376,7 @@ p {
padding: 20px 0; padding: 20px 0;
border: 1px solid #e7e7e7; border: 1px solid #e7e7e7;
box-shadow: 0 2px 0 rgba(0, 0, 0, .03); box-shadow: 0 2px 0 rgba(0, 0, 0, .03);
margin-bottom: 25px; margin: 2rem 0;
} }
.info-panel h1 { .info-panel h1 {

View File

@@ -31,6 +31,10 @@ loop.shared.mixins = (function() {
* @type {Object} * @type {Object}
*/ */
var DropdownMenuMixin = { var DropdownMenuMixin = {
get documentBody() {
return rootObject.document.body;
},
getInitialState: function() { getInitialState: function() {
return {showMenu: false}; return {showMenu: false};
}, },
@@ -40,11 +44,13 @@ loop.shared.mixins = (function() {
}, },
componentDidMount: function() { componentDidMount: function() {
rootObject.document.body.addEventListener("click", this._onBodyClick); this.documentBody.addEventListener("click", this._onBodyClick);
this.documentBody.addEventListener("blur", this.hideDropdownMenu);
}, },
componentWillUnmount: function() { componentWillUnmount: function() {
rootObject.document.body.removeEventListener("click", this._onBodyClick); this.documentBody.removeEventListener("click", this._onBodyClick);
this.documentBody.removeEventListener("blur", this.hideDropdownMenu);
}, },
showDropdownMenu: function() { showDropdownMenu: function() {
@@ -53,7 +59,11 @@ loop.shared.mixins = (function() {
hideDropdownMenu: function() { hideDropdownMenu: function() {
this.setState({showMenu: false}); this.setState({showMenu: false});
} },
toggleDropdownMenu: function() {
this.setState({showMenu: !this.state.showMenu});
},
}; };
/** /**

View File

@@ -29,8 +29,8 @@ loop.shared.models = (function(l10n) {
// requires. // requires.
callType: undefined, // The type of incoming call selected by callType: undefined, // The type of incoming call selected by
// other peer ("audio" or "audio-video") // other peer ("audio" or "audio-video")
selectedCallType: undefined, // The selected type for the call that was selectedCallType: "audio-video", // The selected type for the call that was
// initiated ("audio" or "audio-video") // initiated ("audio" or "audio-video")
callToken: undefined, // Incoming call token. callToken: undefined, // Incoming call token.
// Used for blocking a call url // Used for blocking a call url
subscribedStream: false, // Used to indicate that a stream has been subscribedStream: false, // Used to indicate that a stream has been
@@ -86,8 +86,13 @@ loop.shared.models = (function(l10n) {
/** /**
* Used to indicate that an outgoing call should start any necessary * Used to indicate that an outgoing call should start any necessary
* set-up. * set-up.
*
* @param {String} selectedCallType Call type ("audio" or "audio-video")
*/ */
setupOutgoingCall: function() { setupOutgoingCall: function(selectedCallType) {
if (selectedCallType) {
this.set("selectedCallType", selectedCallType);
}
this.trigger("call:outgoing:setup"); this.trigger("call:outgoing:setup");
}, },

View File

@@ -115,8 +115,9 @@ body,
line-height: 2.2rem; line-height: 2.2rem;
} }
.standalone-btn-label { p.standalone-btn-label {
font-size: 1.2rem; font-size: 1.2rem;
line-height: 1.5rem;
} }
.light-color-font { .light-color-font {

View File

@@ -14,9 +14,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.config = loop.config || {}; loop.config = loop.config || {};
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000"; loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models, var sharedMixins = loop.shared.mixins;
sharedViews = loop.shared.views, var sharedModels = loop.shared.models;
sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views;
var sharedUtils = loop.shared.utils;
/** /**
* Homepage view. * Homepage view.
@@ -116,7 +117,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
render: function() { render: function() {
return ( return (
React.DOM.h1({className: "standalone-header-title"}, React.DOM.h1({className: "standalone-header-title"},
React.DOM.strong(null, mozL10n.get("brandShortname")), " ", mozL10n.get("clientShortname") React.DOM.strong(null, mozL10n.get("brandShortname")),
mozL10n.get("clientShortname")
) )
); );
} }
@@ -305,53 +307,105 @@ loop.webapp = (function($, _, OT, mozL10n) {
React.DOM.div({className: "flex-padding-1"}) React.DOM.div({className: "flex-padding-1"})
) )
), ),
ConversationFooter(null) ConversationFooter(null)
) )
); );
} }
}); });
/** var InitiateCallButton = React.createClass({displayName: 'InitiateCallButton',
* Conversation launcher view. A ConversationModel is associated and attached mixins: [sharedMixins.DropdownMenuMixin],
* as a `model` property.
*
* Required properties:
* - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*/
var StartConversationView = React.createClass({displayName: 'StartConversationView',
propTypes: { propTypes: {
model: React.PropTypes.oneOfType([ caption: React.PropTypes.string.isRequired,
React.PropTypes.instanceOf(sharedModels.ConversationModel), startCall: React.PropTypes.func.isRequired,
React.PropTypes.instanceOf(FxOSConversationModel) disabled: React.PropTypes.bool
]).isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
}, },
getDefaultProps: function() { getDefaultProps: function() {
return {showCallOptionsMenu: false}; return {disabled: false};
},
render: function() {
var dropdownMenuClasses = React.addons.classSet({
"native-dropdown-large-parent": true,
"standalone-dropdown-menu": true,
"visually-hidden": !this.state.showMenu
});
var chevronClasses = React.addons.classSet({
"btn-chevron": true,
"disabled": this.props.disabled
});
return (
React.DOM.div({className: "standalone-btn-chevron-menu-group"},
React.DOM.div({className: "btn-group-chevron"},
React.DOM.div({className: "btn-group"},
React.DOM.button({className: "btn btn-large btn-accept",
onClick: this.props.startCall("audio-video"),
disabled: this.props.disabled,
title: mozL10n.get("initiate_audio_video_call_tooltip2")},
React.DOM.span({className: "standalone-call-btn-text"},
this.props.caption
),
React.DOM.span({className: "standalone-call-btn-video-icon"})
),
React.DOM.div({className: chevronClasses,
onClick: this.toggleDropdownMenu}
)
),
React.DOM.ul({className: dropdownMenuClasses},
React.DOM.li(null,
React.DOM.button({className: "start-audio-only-call",
onClick: this.props.startCall("audio"),
disabled: this.props.disabled},
mozL10n.get("initiate_audio_call_button2")
)
)
)
)
)
);
}
});
/**
* Initiate conversation view.
*/
var InitiateConversationView = React.createClass({displayName: 'InitiateConversationView',
mixins: [Backbone.Events],
propTypes: {
conversation: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(sharedModels.ConversationModel),
React.PropTypes.instanceOf(FxOSConversationModel)
]).isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired,
title: React.PropTypes.string.isRequired,
callButtonLabel: React.PropTypes.string.isRequired
}, },
getInitialState: function() { getInitialState: function() {
return { return {
urlCreationDateString: '', urlCreationDateString: '',
disableCallButton: false, disableCallButton: false
showCallOptionsMenu: this.props.showCallOptionsMenu
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
// Listen for events & hide dropdown menu if user clicks away this.listenTo(this.props.conversation,
window.addEventListener("click", this.clickHandler); "session:error", this._onSessionError);
this.props.model.listenTo(this.props.model, "session:error", this.listenTo(this.props.conversation,
this._onSessionError); "fxos:app-needed", this._onFxOSAppNeeded);
this.props.model.listenTo(this.props.model, "fxos:app-needed", this.props.client.requestCallUrlInfo(
this._onFxOSAppNeeded); this.props.conversation.get("loopToken"),
this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"), this._setConversationTimestamp);
this._setConversationTimestamp); },
componentWillUnmount: function() {
this.stopListening(this.props.conversation);
localStorage.setItem("has-seen-tos", "true");
}, },
_onSessionError: function(error, l10nProps) { _onSessionError: function(error, l10nProps) {
@@ -362,11 +416,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
_onFxOSAppNeeded: function() { _onFxOSAppNeeded: function() {
this.setState({ this.setState({
marketplaceSrc: loop.config.marketplaceUrl marketplaceSrc: loop.config.marketplaceUrl,
}); onMarketplaceMessage: this.props.conversation.onMarketplaceMessage.bind(
this.setState({ this.props.conversation
onMarketplaceMessage: this.props.model.onMarketplaceMessage.bind(
this.props.model
) )
}); });
}, },
@@ -379,11 +431,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
* *
* @param {string} User call type choice "audio" or "audio-video" * @param {string} User call type choice "audio" or "audio-video"
*/ */
_initiateOutgoingCall: function(callType) { startCall: function(callType) {
return function() { return function() {
this.props.model.set("selectedCallType", callType); this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true}); this.setState({disableCallButton: true});
this.props.model.setupOutgoingCall();
}.bind(this); }.bind(this);
}, },
@@ -398,47 +449,21 @@ loop.webapp = (function($, _, OT, mozL10n) {
} }
}, },
componentWillUnmount: function() {
window.removeEventListener("click", this.clickHandler);
localStorage.setItem("has-seen-tos", "true");
},
clickHandler: function(e) {
if (!e.target.classList.contains('btn-chevron') &&
this.state.showCallOptionsMenu) {
this._toggleCallOptionsMenu();
}
},
_toggleCallOptionsMenu: function() {
var state = this.state.showCallOptionsMenu;
this.setState({showCallOptionsMenu: !state});
},
render: function() { render: function() {
var tos_link_name = mozL10n.get("terms_of_use_link_text"); var tosLinkName = mozL10n.get("terms_of_use_link_text");
var privacy_notice_name = mozL10n.get("privacy_notice_link_text"); var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
var tosHTML = mozL10n.get("legal_text_and_links", { var tosHTML = mozL10n.get("legal_text_and_links", {
"terms_of_use_url": "<a target=_blank href='/legal/terms/'>" + "terms_of_use_url": "<a target=_blank href='/legal/terms/'>" +
tos_link_name + "</a>", tosLinkName + "</a>",
"privacy_notice_url": "<a target=_blank href='" + "privacy_notice_url": "<a target=_blank href='" +
"https://www.mozilla.org/privacy/'>" + privacy_notice_name + "</a>" "https://www.mozilla.org/privacy/'>" + privacyNoticeName + "</a>"
}); });
var dropdownMenuClasses = React.addons.classSet({
"native-dropdown-large-parent": true,
"standalone-dropdown-menu": true,
"visually-hidden": !this.state.showCallOptionsMenu
});
var tosClasses = React.addons.classSet({ var tosClasses = React.addons.classSet({
"terms-service": true, "terms-service": true,
hide: (localStorage.getItem("has-seen-tos") === "true") hide: (localStorage.getItem("has-seen-tos") === "true")
}); });
var chevronClasses = React.addons.classSet({
"btn-chevron": true,
"disabled": this.state.disableCallButton
});
return ( return (
React.DOM.div({className: "container"}, React.DOM.div({className: "container"},
@@ -448,47 +473,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
urlCreationDateString: this.state.urlCreationDateString}), urlCreationDateString: this.state.urlCreationDateString}),
React.DOM.p({className: "standalone-btn-label"}, React.DOM.p({className: "standalone-btn-label"},
mozL10n.get("initiate_call_button_label2") this.props.title
), ),
React.DOM.div({id: "messages"}), React.DOM.div({id: "messages"}),
React.DOM.div({className: "btn-group"}, React.DOM.div({className: "btn-group"},
React.DOM.div({className: "flex-padding-1"}), React.DOM.div({className: "flex-padding-1"}),
React.DOM.div({className: "standalone-btn-chevron-menu-group"}, InitiateCallButton({
React.DOM.div({className: "btn-group-chevron"}, caption: this.props.callButtonLabel,
React.DOM.div({className: "btn-group"}, disabled: this.state.disableCallButton,
startCall: this.startCall}
React.DOM.button({className: "btn btn-large btn-accept",
onClick: this._initiateOutgoingCall("audio-video"),
disabled: this.state.disableCallButton,
title: mozL10n.get("initiate_audio_video_call_tooltip2")},
React.DOM.span({className: "standalone-call-btn-text"},
mozL10n.get("initiate_audio_video_call_button2")
),
React.DOM.span({className: "standalone-call-btn-video-icon"})
),
React.DOM.div({className: chevronClasses,
onClick: this._toggleCallOptionsMenu}
)
),
React.DOM.ul({className: dropdownMenuClasses},
React.DOM.li(null,
/*
Button required for disabled state.
*/
React.DOM.button({className: "start-audio-only-call",
onClick: this._initiateOutgoingCall("audio"),
disabled: this.state.disableCallButton},
mozL10n.get("initiate_audio_call_button2")
)
)
)
)
), ),
React.DOM.div({className: "flex-padding-1"}) React.DOM.div({className: "flex-padding-1"})
), ),
@@ -538,6 +533,26 @@ loop.webapp = (function($, _, OT, mozL10n) {
} }
}); });
var StartConversationView = React.createClass({displayName: 'StartConversationView',
render: function() {
return this.transferPropsTo(
InitiateConversationView({
title: mozL10n.get("initiate_call_button_label2"),
callButtonLabel: mozL10n.get("initiate_audio_video_call_button2")})
);
}
});
var FailedConversationView = React.createClass({displayName: 'FailedConversationView',
render: function() {
return this.transferPropsTo(
InitiateConversationView({
title: mozL10n.get("call_failed_title"),
callButtonLabel: mozL10n.get("retry_call_button")})
);
}
});
/** /**
* This view manages the outgoing conversation views - from * This view manages the outgoing conversation views - from
* call initiation through to the actual conversation and call end. * call initiation through to the actual conversation and call end.
@@ -595,11 +610,19 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/ */
render: function() { render: function() {
switch (this.state.callStatus) { switch (this.state.callStatus) {
case "failure":
case "start": { case "start": {
return ( return (
StartConversationView({ StartConversationView({
model: this.props.conversation, conversation: this.props.conversation,
notifications: this.props.notifications,
client: this.props.client}
)
);
}
case "failure": {
return (
FailedConversationView({
conversation: this.props.conversation,
notifications: this.props.notifications, notifications: this.props.notifications,
client: this.props.client} client: this.props.client}
) )
@@ -775,18 +798,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
/** /**
* Handles call rejection. * Handles call rejection.
* *
* @param {String} reason The reason the call was terminated. * @param {String} reason The reason the call was terminated (reject, busy,
* timeout, cancel, media-fail, user-unknown, closed)
*/ */
_handleCallTerminated: function(reason) { _handleCallTerminated: function(reason) {
if (reason !== "cancel") { if (reason === "cancel") {
// XXX This should really display the call failed view - bug 1046959 this.setState({callStatus: "start"});
// will implement this. return;
this.props.notifications.errorL10n("call_timeout_notification_text");
} }
// redirects the user to the call start view // XXX later, we'll want to display more meaningfull messages (needs UX)
// XXX should switch callStatus to failed for specific reasons when we this.props.notifications.errorL10n("call_timeout_notification_text");
// get the call failed view; for now, switch back to start. this.setState({callStatus: "failure"});
this.setState({callStatus: "start"});
}, },
/** /**
@@ -893,6 +915,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
CallUrlExpiredView: CallUrlExpiredView, CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView, PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView, StartConversationView: StartConversationView,
FailedConversationView: FailedConversationView,
OutgoingConversationView: OutgoingConversationView, OutgoingConversationView: OutgoingConversationView,
EndedConversationView: EndedConversationView, EndedConversationView: EndedConversationView,
HomeView: HomeView, HomeView: HomeView,

View File

@@ -14,9 +14,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
loop.config = loop.config || {}; loop.config = loop.config || {};
loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000"; loop.config.serverUrl = loop.config.serverUrl || "http://localhost:5000";
var sharedModels = loop.shared.models, var sharedMixins = loop.shared.mixins;
sharedViews = loop.shared.views, var sharedModels = loop.shared.models;
sharedUtils = loop.shared.utils; var sharedViews = loop.shared.views;
var sharedUtils = loop.shared.utils;
/** /**
* Homepage view. * Homepage view.
@@ -116,7 +117,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
render: function() { render: function() {
return ( return (
<h1 className="standalone-header-title"> <h1 className="standalone-header-title">
<strong>{mozL10n.get("brandShortname")}</strong> {mozL10n.get("clientShortname")} <strong>{mozL10n.get("brandShortname")}</strong>
{mozL10n.get("clientShortname")}
</h1> </h1>
); );
} }
@@ -234,7 +236,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
<h3 className="call-url"> <h3 className="call-url">
{conversationUrl} {conversationUrl}
</h3> </h3>
<h4 className={urlCreationDateClasses} > <h4 className={urlCreationDateClasses}>
{callUrlCreationDateString} {callUrlCreationDateString}
</h4> </h4>
</header> </header>
@@ -286,72 +288,124 @@ loop.webapp = (function($, _, OT, mozL10n) {
<ConversationBranding /> <ConversationBranding />
</header> </header>
<div id="cameraPreview"></div> <div id="cameraPreview" />
<div id="messages"></div> <div id="messages" />
<p className="standalone-btn-label"> <p className="standalone-btn-label">
{callState} {callState}
</p> </p>
<div className="btn-pending-cancel-group btn-group"> <div className="btn-pending-cancel-group btn-group">
<div className="flex-padding-1"></div> <div className="flex-padding-1" />
<button className="btn btn-large btn-cancel" <button className="btn btn-large btn-cancel"
onClick={this._cancelOutgoingCall} > onClick={this._cancelOutgoingCall} >
<span className="standalone-call-btn-text"> <span className="standalone-call-btn-text">
{mozL10n.get("initiate_call_cancel_button")} {mozL10n.get("initiate_call_cancel_button")}
</span> </span>
</button> </button>
<div className="flex-padding-1"></div> <div className="flex-padding-1" />
</div> </div>
</div> </div>
<ConversationFooter /> <ConversationFooter />
</div> </div>
); );
} }
}); });
/** var InitiateCallButton = React.createClass({
* Conversation launcher view. A ConversationModel is associated and attached mixins: [sharedMixins.DropdownMenuMixin],
* as a `model` property.
*
* Required properties:
* - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*/
var StartConversationView = React.createClass({
propTypes: { propTypes: {
model: React.PropTypes.oneOfType([ caption: React.PropTypes.string.isRequired,
React.PropTypes.instanceOf(sharedModels.ConversationModel), startCall: React.PropTypes.func.isRequired,
React.PropTypes.instanceOf(FxOSConversationModel) disabled: React.PropTypes.bool
]).isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
}, },
getDefaultProps: function() { getDefaultProps: function() {
return {showCallOptionsMenu: false}; return {disabled: false};
},
render: function() {
var dropdownMenuClasses = React.addons.classSet({
"native-dropdown-large-parent": true,
"standalone-dropdown-menu": true,
"visually-hidden": !this.state.showMenu
});
var chevronClasses = React.addons.classSet({
"btn-chevron": true,
"disabled": this.props.disabled
});
return (
<div className="standalone-btn-chevron-menu-group">
<div className="btn-group-chevron">
<div className="btn-group">
<button className="btn btn-large btn-accept"
onClick={this.props.startCall("audio-video")}
disabled={this.props.disabled}
title={mozL10n.get("initiate_audio_video_call_tooltip2")}>
<span className="standalone-call-btn-text">
{this.props.caption}
</span>
<span className="standalone-call-btn-video-icon" />
</button>
<div className={chevronClasses}
onClick={this.toggleDropdownMenu}>
</div>
</div>
<ul className={dropdownMenuClasses}>
<li>
<button className="start-audio-only-call"
onClick={this.props.startCall("audio")}
disabled={this.props.disabled}>
{mozL10n.get("initiate_audio_call_button2")}
</button>
</li>
</ul>
</div>
</div>
);
}
});
/**
* Initiate conversation view.
*/
var InitiateConversationView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
conversation: React.PropTypes.oneOfType([
React.PropTypes.instanceOf(sharedModels.ConversationModel),
React.PropTypes.instanceOf(FxOSConversationModel)
]).isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired,
title: React.PropTypes.string.isRequired,
callButtonLabel: React.PropTypes.string.isRequired
}, },
getInitialState: function() { getInitialState: function() {
return { return {
urlCreationDateString: '', urlCreationDateString: '',
disableCallButton: false, disableCallButton: false
showCallOptionsMenu: this.props.showCallOptionsMenu
}; };
}, },
componentDidMount: function() { componentDidMount: function() {
// Listen for events & hide dropdown menu if user clicks away this.listenTo(this.props.conversation,
window.addEventListener("click", this.clickHandler); "session:error", this._onSessionError);
this.props.model.listenTo(this.props.model, "session:error", this.listenTo(this.props.conversation,
this._onSessionError); "fxos:app-needed", this._onFxOSAppNeeded);
this.props.model.listenTo(this.props.model, "fxos:app-needed", this.props.client.requestCallUrlInfo(
this._onFxOSAppNeeded); this.props.conversation.get("loopToken"),
this.props.client.requestCallUrlInfo(this.props.model.get("loopToken"), this._setConversationTimestamp);
this._setConversationTimestamp); },
componentWillUnmount: function() {
this.stopListening(this.props.conversation);
localStorage.setItem("has-seen-tos", "true");
}, },
_onSessionError: function(error, l10nProps) { _onSessionError: function(error, l10nProps) {
@@ -362,11 +416,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
_onFxOSAppNeeded: function() { _onFxOSAppNeeded: function() {
this.setState({ this.setState({
marketplaceSrc: loop.config.marketplaceUrl marketplaceSrc: loop.config.marketplaceUrl,
}); onMarketplaceMessage: this.props.conversation.onMarketplaceMessage.bind(
this.setState({ this.props.conversation
onMarketplaceMessage: this.props.model.onMarketplaceMessage.bind(
this.props.model
) )
}); });
}, },
@@ -379,11 +431,10 @@ loop.webapp = (function($, _, OT, mozL10n) {
* *
* @param {string} User call type choice "audio" or "audio-video" * @param {string} User call type choice "audio" or "audio-video"
*/ */
_initiateOutgoingCall: function(callType) { startCall: function(callType) {
return function() { return function() {
this.props.model.set("selectedCallType", callType); this.props.conversation.setupOutgoingCall(callType);
this.setState({disableCallButton: true}); this.setState({disableCallButton: true});
this.props.model.setupOutgoingCall();
}.bind(this); }.bind(this);
}, },
@@ -398,47 +449,21 @@ loop.webapp = (function($, _, OT, mozL10n) {
} }
}, },
componentWillUnmount: function() {
window.removeEventListener("click", this.clickHandler);
localStorage.setItem("has-seen-tos", "true");
},
clickHandler: function(e) {
if (!e.target.classList.contains('btn-chevron') &&
this.state.showCallOptionsMenu) {
this._toggleCallOptionsMenu();
}
},
_toggleCallOptionsMenu: function() {
var state = this.state.showCallOptionsMenu;
this.setState({showCallOptionsMenu: !state});
},
render: function() { render: function() {
var tos_link_name = mozL10n.get("terms_of_use_link_text"); var tosLinkName = mozL10n.get("terms_of_use_link_text");
var privacy_notice_name = mozL10n.get("privacy_notice_link_text"); var privacyNoticeName = mozL10n.get("privacy_notice_link_text");
var tosHTML = mozL10n.get("legal_text_and_links", { var tosHTML = mozL10n.get("legal_text_and_links", {
"terms_of_use_url": "<a target=_blank href='/legal/terms/'>" + "terms_of_use_url": "<a target=_blank href='/legal/terms/'>" +
tos_link_name + "</a>", tosLinkName + "</a>",
"privacy_notice_url": "<a target=_blank href='" + "privacy_notice_url": "<a target=_blank href='" +
"https://www.mozilla.org/privacy/'>" + privacy_notice_name + "</a>" "https://www.mozilla.org/privacy/'>" + privacyNoticeName + "</a>"
}); });
var dropdownMenuClasses = React.addons.classSet({
"native-dropdown-large-parent": true,
"standalone-dropdown-menu": true,
"visually-hidden": !this.state.showCallOptionsMenu
});
var tosClasses = React.addons.classSet({ var tosClasses = React.addons.classSet({
"terms-service": true, "terms-service": true,
hide: (localStorage.getItem("has-seen-tos") === "true") hide: (localStorage.getItem("has-seen-tos") === "true")
}); });
var chevronClasses = React.addons.classSet({
"btn-chevron": true,
"disabled": this.state.disableCallButton
});
return ( return (
<div className="container"> <div className="container">
@@ -448,49 +473,19 @@ loop.webapp = (function($, _, OT, mozL10n) {
urlCreationDateString={this.state.urlCreationDateString} /> urlCreationDateString={this.state.urlCreationDateString} />
<p className="standalone-btn-label"> <p className="standalone-btn-label">
{mozL10n.get("initiate_call_button_label2")} {this.props.title}
</p> </p>
<div id="messages"></div> <div id="messages"></div>
<div className="btn-group"> <div className="btn-group">
<div className="flex-padding-1"></div> <div className="flex-padding-1" />
<div className="standalone-btn-chevron-menu-group"> <InitiateCallButton
<div className="btn-group-chevron"> caption={this.props.callButtonLabel}
<div className="btn-group"> disabled={this.state.disableCallButton}
startCall={this.startCall}
<button className="btn btn-large btn-accept" />
onClick={this._initiateOutgoingCall("audio-video")} <div className="flex-padding-1" />
disabled={this.state.disableCallButton}
title={mozL10n.get("initiate_audio_video_call_tooltip2")} >
<span className="standalone-call-btn-text">
{mozL10n.get("initiate_audio_video_call_button2")}
</span>
<span className="standalone-call-btn-video-icon"></span>
</button>
<div className={chevronClasses}
onClick={this._toggleCallOptionsMenu}>
</div>
</div>
<ul className={dropdownMenuClasses}>
<li>
{/*
Button required for disabled state.
*/}
<button className="start-audio-only-call"
onClick={this._initiateOutgoingCall("audio")}
disabled={this.state.disableCallButton} >
{mozL10n.get("initiate_audio_call_button2")}
</button>
</li>
</ul>
</div>
</div>
<div className="flex-padding-1"></div>
</div> </div>
<p className={tosClasses} <p className={tosClasses}
@@ -538,6 +533,26 @@ loop.webapp = (function($, _, OT, mozL10n) {
} }
}); });
var StartConversationView = React.createClass({
render: function() {
return this.transferPropsTo(
<InitiateConversationView
title={mozL10n.get("initiate_call_button_label2")}
callButtonLabel={mozL10n.get("initiate_audio_video_call_button2")} />
);
}
});
var FailedConversationView = React.createClass({
render: function() {
return this.transferPropsTo(
<InitiateConversationView
title={mozL10n.get("call_failed_title")}
callButtonLabel={mozL10n.get("retry_call_button")} />
);
}
});
/** /**
* This view manages the outgoing conversation views - from * This view manages the outgoing conversation views - from
* call initiation through to the actual conversation and call end. * call initiation through to the actual conversation and call end.
@@ -595,11 +610,19 @@ loop.webapp = (function($, _, OT, mozL10n) {
*/ */
render: function() { render: function() {
switch (this.state.callStatus) { switch (this.state.callStatus) {
case "failure":
case "start": { case "start": {
return ( return (
<StartConversationView <StartConversationView
model={this.props.conversation} conversation={this.props.conversation}
notifications={this.props.notifications}
client={this.props.client}
/>
);
}
case "failure": {
return (
<FailedConversationView
conversation={this.props.conversation}
notifications={this.props.notifications} notifications={this.props.notifications}
client={this.props.client} client={this.props.client}
/> />
@@ -775,18 +798,17 @@ loop.webapp = (function($, _, OT, mozL10n) {
/** /**
* Handles call rejection. * Handles call rejection.
* *
* @param {String} reason The reason the call was terminated. * @param {String} reason The reason the call was terminated (reject, busy,
* timeout, cancel, media-fail, user-unknown, closed)
*/ */
_handleCallTerminated: function(reason) { _handleCallTerminated: function(reason) {
if (reason !== "cancel") { if (reason === "cancel") {
// XXX This should really display the call failed view - bug 1046959 this.setState({callStatus: "start"});
// will implement this. return;
this.props.notifications.errorL10n("call_timeout_notification_text");
} }
// redirects the user to the call start view // XXX later, we'll want to display more meaningfull messages (needs UX)
// XXX should switch callStatus to failed for specific reasons when we this.props.notifications.errorL10n("call_timeout_notification_text");
// get the call failed view; for now, switch back to start. this.setState({callStatus: "failure"});
this.setState({callStatus: "start"});
}, },
/** /**
@@ -893,6 +915,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
CallUrlExpiredView: CallUrlExpiredView, CallUrlExpiredView: CallUrlExpiredView,
PendingConversationView: PendingConversationView, PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView, StartConversationView: StartConversationView,
FailedConversationView: FailedConversationView,
OutgoingConversationView: OutgoingConversationView, OutgoingConversationView: OutgoingConversationView,
EndedConversationView: EndedConversationView, EndedConversationView: EndedConversationView,
HomeView: HomeView, HomeView: HomeView,

View File

@@ -5,6 +5,7 @@ call_timeout_notification_text=Your call did not go through.
missing_conversation_info=Missing conversation information. missing_conversation_info=Missing conversation information.
network_disconnected=The network connection terminated abruptly. network_disconnected=The network connection terminated abruptly.
peer_ended_conversation2=The person you were calling has ended the conversation. peer_ended_conversation2=The person you were calling has ended the conversation.
call_failed_title=Call failed.
connection_error_see_console_notification=Call failed; see console for details. connection_error_see_console_notification=Call failed; see console for details.
generic_failure_title=Something went wrong. generic_failure_title=Something went wrong.
generic_failure_with_reason2=You can try again or email a link to be reached at later. generic_failure_with_reason2=You can try again or email a link to be reached at later.

View File

@@ -76,6 +76,19 @@ describe("loop.shared.models", function() {
}); });
describe("#setupOutgoingCall", function() { describe("#setupOutgoingCall", function() {
it("should set the a custom selected call type", function() {
conversation.setupOutgoingCall("audio");
expect(conversation.get("selectedCallType")).eql("audio");
});
it("should respect the default selected call type when none is passed",
function() {
conversation.setupOutgoingCall();
expect(conversation.get("selectedCallType")).eql("audio-video");
});
it("should trigger a `call:outgoing:setup` event", function(done) { it("should trigger a `call:outgoing:setup` event", function(done) {
conversation.once("call:outgoing:setup", function() { conversation.once("call:outgoing:setup", function() {
done(); done();

View File

@@ -196,14 +196,14 @@ describe("loop.webapp", function() {
sandbox.stub(notifications, "errorL10n"); sandbox.stub(notifications, "errorL10n");
}); });
it("should display the StartConversationView", function() { it("should display the FailedConversationView", function() {
ocView._websocket.trigger("progress", { ocView._websocket.trigger("progress", {
state: "terminated", state: "terminated",
reason: "reject" reason: "reject"
}); });
TestUtils.findRenderedComponentWithType(ocView, TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView); loop.webapp.FailedConversationView);
}); });
it("should display an error message if the reason is not 'cancel'", it("should display an error message if the reason is not 'cancel'",
@@ -271,14 +271,14 @@ describe("loop.webapp", function() {
}); });
describe("call:outgoing", function() { describe("call:outgoing", function() {
it("should set display the StartConversationView if session token is missing", it("should display FailedConversationView if session token is missing",
function() { function() {
conversation.set("loopToken", ""); conversation.set("loopToken", "");
ocView.startCall(); ocView.startCall();
TestUtils.findRenderedComponentWithType(ocView, TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView); loop.webapp.FailedConversationView);
}); });
it("should notify the user if session token is missing", function() { it("should notify the user if session token is missing", function() {
@@ -400,11 +400,11 @@ describe("loop.webapp", function() {
conversation.set("loopToken", ""); conversation.set("loopToken", "");
}); });
it("should set display the StartConversationView", function() { it("should display the FailedConversationView", function() {
conversation.setupOutgoingCall(); conversation.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView, TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView); loop.webapp.FailedConversationView);
}); });
it("should display an error", function() { it("should display an error", function() {
@@ -416,13 +416,12 @@ describe("loop.webapp", function() {
describe("Has loop token", function() { describe("Has loop token", function() {
beforeEach(function() { beforeEach(function() {
conversation.set("selectedCallType", "audio-video");
sandbox.stub(conversation, "outgoing"); sandbox.stub(conversation, "outgoing");
}); });
it("should call requestCallInfo on the client", it("should call requestCallInfo on the client",
function() { function() {
conversation.setupOutgoingCall(); conversation.setupOutgoingCall("audio-video");
sinon.assert.calledOnce(client.requestCallInfo); sinon.assert.calledOnce(client.requestCallInfo);
sinon.assert.calledWith(client.requestCallInfo, "fakeToken", sinon.assert.calledWith(client.requestCallInfo, "fakeToken",
@@ -440,14 +439,14 @@ describe("loop.webapp", function() {
loop.webapp.CallUrlExpiredView); loop.webapp.CallUrlExpiredView);
}); });
it("should set display the StartConversationView on any other error", it("should set display the FailedConversationView on any other error",
function() { function() {
client.requestCallInfo.callsArgWith(2, {errno: 104}); client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall(); conversation.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView, TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView); loop.webapp.FailedConversationView);
}); });
it("should notify the user on any other error", function() { it("should notify the user on any other error", function() {
@@ -585,8 +584,7 @@ describe("loop.webapp", function() {
describe("StartConversationView", function() { describe("StartConversationView", function() {
describe("#initiate", function() { describe("#initiate", function() {
var conversation, setupOutgoingCall, view, fakeSubmitEvent, var conversation, view, fakeSubmitEvent, requestCallUrlInfo;
requestCallUrlInfo;
beforeEach(function() { beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, { conversation = new sharedModels.ConversationModel({}, {
@@ -594,7 +592,6 @@ describe("loop.webapp", function() {
}); });
fakeSubmitEvent = {preventDefault: sinon.spy()}; fakeSubmitEvent = {preventDefault: sinon.spy()};
setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var standaloneClientStub = { var standaloneClientStub = {
requestCallUrlInfo: function(token, cb) { requestCallUrlInfo: function(token, cb) {
@@ -605,7 +602,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, conversation: conversation,
notifications: notifications, notifications: notifications,
client: standaloneClientStub client: standaloneClientStub
}) })
@@ -614,20 +611,24 @@ describe("loop.webapp", function() {
it("should start the audio-video conversation establishment process", it("should start the audio-video conversation establishment process",
function() { function() {
var setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var button = view.getDOMNode().querySelector(".btn-accept"); var button = view.getDOMNode().querySelector(".btn-accept");
React.addons.TestUtils.Simulate.click(button); React.addons.TestUtils.Simulate.click(button);
sinon.assert.calledOnce(setupOutgoingCall); sinon.assert.calledOnce(setupOutgoingCall);
sinon.assert.calledWithExactly(setupOutgoingCall); sinon.assert.calledWithExactly(setupOutgoingCall, "audio-video");
}); });
it("should start the audio-only conversation establishment process", it("should start the audio-only conversation establishment process",
function() { function() {
var setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var button = view.getDOMNode().querySelector(".start-audio-only-call"); var button = view.getDOMNode().querySelector(".start-audio-only-call");
React.addons.TestUtils.Simulate.click(button); React.addons.TestUtils.Simulate.click(button);
sinon.assert.calledOnce(setupOutgoingCall); sinon.assert.calledOnce(setupOutgoingCall);
sinon.assert.calledWithExactly(setupOutgoingCall); sinon.assert.calledWithExactly(setupOutgoingCall, "audio");
}); });
it("should disable audio-video button once session is initiated", it("should disable audio-video button once session is initiated",
@@ -650,35 +651,35 @@ describe("loop.webapp", function() {
expect(button.disabled).to.eql(true); expect(button.disabled).to.eql(true);
}); });
it("should set selectedCallType to audio", function() { it("should set selectedCallType to audio", function() {
conversation.set("loopToken", "fake"); conversation.set("loopToken", "fake");
var button = view.getDOMNode().querySelector(".start-audio-only-call"); var button = view.getDOMNode().querySelector(".start-audio-only-call");
React.addons.TestUtils.Simulate.click(button); React.addons.TestUtils.Simulate.click(button);
expect(conversation.get("selectedCallType")).to.eql("audio"); expect(conversation.get("selectedCallType")).to.eql("audio");
});
it("should set selectedCallType to audio-video", function() {
conversation.set("loopToken", "fake");
var button = view.getDOMNode().querySelector(".standalone-call-btn-video-icon");
React.addons.TestUtils.Simulate.click(button);
expect(conversation.get("selectedCallType")).to.eql("audio-video");
});
it("should set state.urlCreationDateString to a locale date string",
function() {
// wrap in a jquery object because text is broken up
// into several span elements
var date = new Date(0);
var options = {year: "numeric", month: "long", day: "numeric"};
var timestamp = date.toLocaleDateString(navigator.language, options);
expect(view.state.urlCreationDateString).to.eql(timestamp);
}); });
it("should set selectedCallType to audio-video", function() {
conversation.set("loopToken", "fake");
var button = view.getDOMNode().querySelector(".standalone-call-btn-video-icon");
React.addons.TestUtils.Simulate.click(button);
expect(conversation.get("selectedCallType")).to.eql("audio-video");
});
// XXX this test breaks while the feature actually works; find a way to
// test this properly.
it.skip("should set state.urlCreationDateString to a locale date string",
function() {
var date = new Date();
var options = {year: "numeric", month: "long", day: "numeric"};
var timestamp = date.toLocaleDateString(navigator.language, options);
var dateElem = view.getDOMNode().querySelector(".call-url-date");
expect(dateElem.textContent).to.eql(timestamp);
});
}); });
describe("Events", function() { describe("Events", function() {
@@ -697,7 +698,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, conversation: conversation,
notifications: notifications, notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo} client: {requestCallUrlInfo: requestCallUrlInfo}
}) })
@@ -782,7 +783,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, conversation: conversation,
notifications: notifications, notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo} client: {requestCallUrlInfo: requestCallUrlInfo}
}) })
@@ -798,7 +799,7 @@ describe("loop.webapp", function() {
localStorage.setItem("has-seen-tos", "true"); localStorage.setItem("has-seen-tos", "true");
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, conversation: conversation,
notifications: notifications, notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo} client: {requestCallUrlInfo: requestCallUrlInfo}
}) })
@@ -888,7 +889,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: conversation, conversation: conversation,
notifications: notifications, notifications: notifications,
client: standaloneClientStub client: standaloneClientStub
}) })
@@ -1003,7 +1004,7 @@ describe("loop.webapp", function() {
before(function() { before(function() {
view = React.addons.TestUtils.renderIntoDocument( view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({ loop.webapp.StartConversationView({
model: model, conversation: model,
notifications: notifications, notifications: notifications,
client: {requestCallUrlInfo: sandbox.stub()} client: {requestCallUrlInfo: sandbox.stub()}
}) })

View File

@@ -18,12 +18,13 @@
// 2. Standalone webapp // 2. Standalone webapp
var HomeView = loop.webapp.HomeView; var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView; var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView; var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView; var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView; var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView; var StartConversationView = loop.webapp.StartConversationView;
var EndedConversationView = loop.webapp.EndedConversationView; var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components // 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar; var ConversationToolbar = loop.shared.views.ConversationToolbar;
@@ -168,8 +169,7 @@
Example({summary: "Default", dashed: "true", style: {width: "260px", height: "254px"}}, Example({summary: "Default", dashed: "true", style: {width: "260px", height: "254px"}},
React.DOM.div({className: "fx-embedded"}, React.DOM.div({className: "fx-embedded"},
IncomingCallView({model: mockConversationModel, IncomingCallView({model: mockConversationModel,
showDeclineMenu: true, showMenu: true})
video: true})
) )
) )
), ),
@@ -236,10 +236,19 @@
Section({name: "StartConversationView"}, Section({name: "StartConversationView"},
Example({summary: "Start conversation view", dashed: "true"}, Example({summary: "Start conversation view", dashed: "true"},
React.DOM.div({className: "standalone"}, React.DOM.div({className: "standalone"},
StartConversationView({model: mockConversationModel, StartConversationView({conversation: mockConversationModel,
client: mockClient, client: mockClient,
notifications: notifications, notifications: notifications})
showCallOptionsMenu: true}) )
)
),
Section({name: "FailedConversationView"},
Example({summary: "Failed conversation view", dashed: "true"},
React.DOM.div({className: "standalone"},
FailedConversationView({conversation: mockConversationModel,
client: mockClient,
notifications: notifications})
) )
) )
), ),

View File

@@ -18,12 +18,13 @@
// 2. Standalone webapp // 2. Standalone webapp
var HomeView = loop.webapp.HomeView; var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView; var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView; var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView; var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView; var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView; var StartConversationView = loop.webapp.StartConversationView;
var EndedConversationView = loop.webapp.EndedConversationView; var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components // 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar; var ConversationToolbar = loop.shared.views.ConversationToolbar;
@@ -168,8 +169,7 @@
<Example summary="Default" dashed="true" style={{width: "260px", height: "254px"}}> <Example summary="Default" dashed="true" style={{width: "260px", height: "254px"}}>
<div className="fx-embedded" > <div className="fx-embedded" >
<IncomingCallView model={mockConversationModel} <IncomingCallView model={mockConversationModel}
showDeclineMenu={true} showMenu={true} />
video={true} />
</div> </div>
</Example> </Example>
</Section> </Section>
@@ -236,10 +236,19 @@
<Section name="StartConversationView"> <Section name="StartConversationView">
<Example summary="Start conversation view" dashed="true"> <Example summary="Start conversation view" dashed="true">
<div className="standalone"> <div className="standalone">
<StartConversationView model={mockConversationModel} <StartConversationView conversation={mockConversationModel}
client={mockClient} client={mockClient}
notifications={notifications} notifications={notifications} />
showCallOptionsMenu={true} /> </div>
</Example>
</Section>
<Section name="FailedConversationView">
<Example summary="Failed conversation view" dashed="true">
<div className="standalone">
<FailedConversationView conversation={mockConversationModel}
client={mockClient}
notifications={notifications} />
</div> </div>
</Example> </Example>
</Section> </Section>