Backed out changeset 4c48e36e05bb (bug 1000240) for marionette failures

This commit is contained in:
Wes Kocher 2014-09-26 16:31:30 -07:00
parent ef3d69de82
commit fb1ba03717
13 changed files with 384 additions and 434 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -5,7 +5,6 @@ call_timeout_notification_text=Your call did not go through.
missing_conversation_info=Missing conversation information.
network_disconnected=The network connection terminated abruptly.
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.
generic_failure_title=Something went wrong.
generic_failure_with_reason2=You can try again or email a link to be reached at later.

View File

@ -76,19 +76,6 @@ describe("loop.shared.models", 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) {
conversation.once("call:outgoing:setup", function() {
done();

View File

@ -196,14 +196,14 @@ describe("loop.webapp", function() {
sandbox.stub(notifications, "errorL10n");
});
it("should display the FailedConversationView", function() {
it("should display the StartConversationView", function() {
ocView._websocket.trigger("progress", {
state: "terminated",
reason: "reject"
});
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.FailedConversationView);
loop.webapp.StartConversationView);
});
it("should display an error message if the reason is not 'cancel'",
@ -271,14 +271,14 @@ describe("loop.webapp", function() {
});
describe("call:outgoing", function() {
it("should display FailedConversationView if session token is missing",
it("should set display the StartConversationView if session token is missing",
function() {
conversation.set("loopToken", "");
ocView.startCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.FailedConversationView);
loop.webapp.StartConversationView);
});
it("should notify the user if session token is missing", function() {
@ -400,11 +400,11 @@ describe("loop.webapp", function() {
conversation.set("loopToken", "");
});
it("should display the FailedConversationView", function() {
it("should set display the StartConversationView", function() {
conversation.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.FailedConversationView);
loop.webapp.StartConversationView);
});
it("should display an error", function() {
@ -416,12 +416,13 @@ describe("loop.webapp", function() {
describe("Has loop token", function() {
beforeEach(function() {
conversation.set("selectedCallType", "audio-video");
sandbox.stub(conversation, "outgoing");
});
it("should call requestCallInfo on the client",
function() {
conversation.setupOutgoingCall("audio-video");
conversation.setupOutgoingCall();
sinon.assert.calledOnce(client.requestCallInfo);
sinon.assert.calledWith(client.requestCallInfo, "fakeToken",
@ -439,14 +440,14 @@ describe("loop.webapp", function() {
loop.webapp.CallUrlExpiredView);
});
it("should set display the FailedConversationView on any other error",
it("should set display the StartConversationView on any other error",
function() {
client.requestCallInfo.callsArgWith(2, {errno: 104});
conversation.setupOutgoingCall();
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.FailedConversationView);
loop.webapp.StartConversationView);
});
it("should notify the user on any other error", function() {
@ -584,7 +585,8 @@ describe("loop.webapp", function() {
describe("StartConversationView", function() {
describe("#initiate", function() {
var conversation, view, fakeSubmitEvent, requestCallUrlInfo;
var conversation, setupOutgoingCall, view, fakeSubmitEvent,
requestCallUrlInfo;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {
@ -592,6 +594,7 @@ describe("loop.webapp", function() {
});
fakeSubmitEvent = {preventDefault: sinon.spy()};
setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var standaloneClientStub = {
requestCallUrlInfo: function(token, cb) {
@ -602,7 +605,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({
conversation: conversation,
model: conversation,
notifications: notifications,
client: standaloneClientStub
})
@ -611,24 +614,20 @@ describe("loop.webapp", function() {
it("should start the audio-video conversation establishment process",
function() {
var setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var button = view.getDOMNode().querySelector(".btn-accept");
React.addons.TestUtils.Simulate.click(button);
sinon.assert.calledOnce(setupOutgoingCall);
sinon.assert.calledWithExactly(setupOutgoingCall, "audio-video");
sinon.assert.calledWithExactly(setupOutgoingCall);
});
it("should start the audio-only conversation establishment process",
function() {
var setupOutgoingCall = sinon.stub(conversation, "setupOutgoingCall");
var button = view.getDOMNode().querySelector(".start-audio-only-call");
React.addons.TestUtils.Simulate.click(button);
sinon.assert.calledOnce(setupOutgoingCall);
sinon.assert.calledWithExactly(setupOutgoingCall, "audio");
sinon.assert.calledWithExactly(setupOutgoingCall);
});
it("should disable audio-video button once session is initiated",
@ -651,35 +650,35 @@ describe("loop.webapp", function() {
expect(button.disabled).to.eql(true);
});
it("should set selectedCallType to audio", function() {
conversation.set("loopToken", "fake");
it("should set selectedCallType to audio", function() {
conversation.set("loopToken", "fake");
var button = view.getDOMNode().querySelector(".start-audio-only-call");
React.addons.TestUtils.Simulate.click(button);
var button = view.getDOMNode().querySelector(".start-audio-only-call");
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() {
@ -698,7 +697,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({
conversation: conversation,
model: conversation,
notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo}
})
@ -783,7 +782,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({
conversation: conversation,
model: conversation,
notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo}
})
@ -799,7 +798,7 @@ describe("loop.webapp", function() {
localStorage.setItem("has-seen-tos", "true");
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({
conversation: conversation,
model: conversation,
notifications: notifications,
client: {requestCallUrlInfo: requestCallUrlInfo}
})
@ -889,7 +888,7 @@ describe("loop.webapp", function() {
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({
conversation: conversation,
model: conversation,
notifications: notifications,
client: standaloneClientStub
})
@ -1004,7 +1003,7 @@ describe("loop.webapp", function() {
before(function() {
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.StartConversationView({
conversation: model,
model: model,
notifications: notifications,
client: {requestCallUrlInfo: sandbox.stub()}
})

View File

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

View File

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