mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-12-01 17:23:59 +00:00
Bug 972992 (Part 1): Loop desktop client user feedback form. r=Standard8
This commit is contained in:
parent
3e6505c039
commit
96c36a5dc5
@ -86,27 +86,27 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"visually-hidden": !this.state.showDeclineMenu
|
||||
});
|
||||
return (
|
||||
React.DOM.div({className: conversationPanelClass},
|
||||
React.DOM.h2(null, __("incoming_call")),
|
||||
React.DOM.div({className: "button-group incoming-call-action-group"},
|
||||
React.DOM.div({className: "button-chevron-menu-group"},
|
||||
React.DOM.div({className: "button-group-chevron"},
|
||||
React.DOM.div({className: "button-group"},
|
||||
React.DOM.button({className: btnClassDecline, onClick: this._handleDecline},
|
||||
React.DOM.div( {className:conversationPanelClass},
|
||||
React.DOM.h2(null, __("incoming_call")),
|
||||
React.DOM.div( {className:"button-group incoming-call-action-group"},
|
||||
React.DOM.div( {className:"button-chevron-menu-group"},
|
||||
React.DOM.div( {className:"button-group-chevron"},
|
||||
React.DOM.div( {className:"button-group"},
|
||||
React.DOM.button( {className:btnClassDecline, onClick:this._handleDecline},
|
||||
__("incoming_call_decline_button")
|
||||
),
|
||||
React.DOM.div({className: "btn-chevron",
|
||||
onClick: this._toggleDeclineMenu}
|
||||
),
|
||||
React.DOM.div( {className:"btn-chevron",
|
||||
onClick:this._toggleDeclineMenu}
|
||||
)
|
||||
),
|
||||
React.DOM.ul({className: declineDropdownMenuClasses},
|
||||
React.DOM.li({className: "btn-block", onClick: this._handleDeclineBlock},
|
||||
),
|
||||
React.DOM.ul( {className:declineDropdownMenuClasses},
|
||||
React.DOM.li( {className:"btn-block", onClick:this._handleDeclineBlock},
|
||||
__("incoming_call_decline_and_block_button")
|
||||
)
|
||||
)
|
||||
)
|
||||
),
|
||||
React.DOM.button({className: btnClassAccept, onClick: this._handleAccept},
|
||||
),
|
||||
React.DOM.button( {className:btnClassAccept, onClick:this._handleAccept},
|
||||
__("incoming_call_answer_button")
|
||||
)
|
||||
)
|
||||
@ -116,30 +116,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call ended view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
var EndedCallView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<p>',
|
||||
' <button class="btn btn-info" data-l10n-id="close_window"></button>',
|
||||
'</p>'
|
||||
].join("")),
|
||||
|
||||
className: "call-ended",
|
||||
|
||||
events: {
|
||||
"click button": "closeWindow"
|
||||
},
|
||||
|
||||
closeWindow: function(event) {
|
||||
event.preventDefault();
|
||||
// XXX For now, we just close the window.
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation router.
|
||||
*
|
||||
@ -156,7 +132,8 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended",
|
||||
"call/declineAndBlock": "declineAndBlock"
|
||||
"call/declineAndBlock": "declineAndBlock",
|
||||
"call/feedback": "feedback"
|
||||
},
|
||||
|
||||
/**
|
||||
@ -170,7 +147,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* @override {loop.shared.router.BaseConversationRouter.endCall}
|
||||
*/
|
||||
endCall: function() {
|
||||
this.navigate("call/ended", {trigger: true});
|
||||
this.navigate("call/feedback", {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -254,10 +231,17 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
},
|
||||
|
||||
/**
|
||||
* XXX: load a view with a close button for now?
|
||||
* Call has ended, display a feedback form.
|
||||
*/
|
||||
ended: function() {
|
||||
this.loadView(new EndedCallView());
|
||||
feedback: function() {
|
||||
this.loadReactComponent(sharedViews.FeedbackView({
|
||||
// XXX for now we pass in a fake feeback API client (see bug 972992)
|
||||
feedbackApiClient: {
|
||||
send: function(fields, cb) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@ -280,7 +264,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
|
||||
return {
|
||||
ConversationRouter: ConversationRouter,
|
||||
EndedCallView: EndedCallView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
};
|
||||
|
@ -116,30 +116,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Call ended view.
|
||||
* @type {loop.shared.views.BaseView}
|
||||
*/
|
||||
var EndedCallView = sharedViews.BaseView.extend({
|
||||
template: _.template([
|
||||
'<p>',
|
||||
' <button class="btn btn-info" data-l10n-id="close_window"></button>',
|
||||
'</p>'
|
||||
].join("")),
|
||||
|
||||
className: "call-ended",
|
||||
|
||||
events: {
|
||||
"click button": "closeWindow"
|
||||
},
|
||||
|
||||
closeWindow: function(event) {
|
||||
event.preventDefault();
|
||||
// XXX For now, we just close the window.
|
||||
window.close();
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Conversation router.
|
||||
*
|
||||
@ -156,7 +132,8 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
"call/decline": "decline",
|
||||
"call/ongoing": "conversation",
|
||||
"call/ended": "ended",
|
||||
"call/declineAndBlock": "declineAndBlock"
|
||||
"call/declineAndBlock": "declineAndBlock",
|
||||
"call/feedback": "feedback"
|
||||
},
|
||||
|
||||
/**
|
||||
@ -170,7 +147,7 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
* @override {loop.shared.router.BaseConversationRouter.endCall}
|
||||
*/
|
||||
endCall: function() {
|
||||
this.navigate("call/ended", {trigger: true});
|
||||
this.navigate("call/feedback", {trigger: true});
|
||||
},
|
||||
|
||||
/**
|
||||
@ -254,10 +231,17 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
},
|
||||
|
||||
/**
|
||||
* XXX: load a view with a close button for now?
|
||||
* Call has ended, display a feedback form.
|
||||
*/
|
||||
ended: function() {
|
||||
this.loadView(new EndedCallView());
|
||||
feedback: function() {
|
||||
this.loadReactComponent(sharedViews.FeedbackView({
|
||||
// XXX for now we pass in a fake feeback API client (see bug 972992)
|
||||
feedbackApiClient: {
|
||||
send: function(fields, cb) {
|
||||
cb();
|
||||
}
|
||||
}
|
||||
}));
|
||||
}
|
||||
});
|
||||
|
||||
@ -280,7 +264,6 @@ loop.conversation = (function(OT, mozL10n) {
|
||||
|
||||
return {
|
||||
ConversationRouter: ConversationRouter,
|
||||
EndedCallView: EndedCallView,
|
||||
IncomingCallView: IncomingCallView,
|
||||
init: init
|
||||
};
|
||||
|
@ -238,3 +238,105 @@
|
||||
color: #FFF;
|
||||
background: #111;
|
||||
}
|
||||
|
||||
/* Expired call url page */
|
||||
|
||||
.expired-url-info {
|
||||
width: 400px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
.promote-firefox {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
line-height: 24px;
|
||||
margin: 2em 0;
|
||||
}
|
||||
|
||||
.promote-firefox h3 {
|
||||
font-weight: 300;
|
||||
}
|
||||
|
||||
/* Feedback form */
|
||||
|
||||
.feedback {
|
||||
padding: 1em;
|
||||
}
|
||||
|
||||
.feedback h3 {
|
||||
color: #666;
|
||||
font-size: 12px;
|
||||
font-weight: 700;
|
||||
text-align: center;
|
||||
margin-bottom: 10px;
|
||||
margin-top: 15px;
|
||||
}
|
||||
|
||||
.feedback .faces {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 20px 0;
|
||||
}
|
||||
|
||||
.feedback .face {
|
||||
border: 1px solid transparent;
|
||||
box-shadow: 0px 1px 2px #CCC;
|
||||
cursor: pointer;
|
||||
border-radius: 4px;
|
||||
margin: 0px 10px;
|
||||
width: 80px;
|
||||
height: 80px;
|
||||
background-color: #fbfbfb;
|
||||
background-size: 60px auto;
|
||||
background-position: center center;
|
||||
background-repeat: no-repeat;
|
||||
}
|
||||
|
||||
.feedback .face:hover {
|
||||
border: 1px solid #DDD;
|
||||
background-color: #FEFEFE;
|
||||
}
|
||||
|
||||
.feedback .face.face-happy {
|
||||
background-image: url("../img/happy.png");
|
||||
}
|
||||
|
||||
.feedback .face.face-sad {
|
||||
background-image: url("../img/sad.png");
|
||||
}
|
||||
|
||||
.feedback button.back {
|
||||
border-radius: 2px;
|
||||
border: 1px solid #CCC;
|
||||
color: #CCC;
|
||||
font-size: 11px;
|
||||
cursor: pointer;
|
||||
padding: 3px 10px;
|
||||
display: inline;
|
||||
margin-bottom: 1em;
|
||||
}
|
||||
|
||||
.feedback label {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.feedback form input[type="radio"] {
|
||||
margin-right: .5em;
|
||||
}
|
||||
|
||||
.feedback form button[type="submit"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feedback form input[type="text"] {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.feedback .info {
|
||||
display: block;
|
||||
font-size: 10px;
|
||||
color: #CCC;
|
||||
text-align: center;
|
||||
}
|
||||
|
BIN
browser/components/loop/content/shared/img/happy.png
Normal file
BIN
browser/components/loop/content/shared/img/happy.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 39 KiB |
BIN
browser/components/loop/content/shared/img/sad.png
Normal file
BIN
browser/components/loop/content/shared/img/sad.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -13,6 +13,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
|
||||
var sharedModels = loop.shared.models;
|
||||
var __ = l10n.get;
|
||||
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
||||
|
||||
/**
|
||||
* L10n view. Translates resulting view DOM fragment once rendered.
|
||||
@ -139,9 +140,9 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
render: function() {
|
||||
return (
|
||||
/* jshint ignore:start */
|
||||
React.DOM.button({className: this._getClasses(),
|
||||
title: this._getTitle(),
|
||||
onClick: this.handleClick})
|
||||
React.DOM.button( {className:this._getClasses(),
|
||||
title:this._getTitle(),
|
||||
onClick:this.handleClick})
|
||||
/* jshint ignore:end */
|
||||
);
|
||||
}
|
||||
@ -180,16 +181,16 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
render: function() {
|
||||
/* jshint ignore:start */
|
||||
return (
|
||||
React.DOM.ul({className: "conversation-toolbar"},
|
||||
React.DOM.li(null, React.DOM.button({className: "btn btn-hangup",
|
||||
onClick: this.handleClickHangup,
|
||||
title: __("hangup_button_title")})),
|
||||
React.DOM.li(null, MediaControlButton({action: this.handleToggleVideo,
|
||||
enabled: this.props.video.enabled,
|
||||
scope: "local", type: "video"})),
|
||||
React.DOM.li(null, MediaControlButton({action: this.handleToggleAudio,
|
||||
enabled: this.props.audio.enabled,
|
||||
scope: "local", type: "audio"}))
|
||||
React.DOM.ul( {className:"conversation-toolbar"},
|
||||
React.DOM.li(null, React.DOM.button( {className:"btn btn-hangup",
|
||||
onClick:this.handleClickHangup,
|
||||
title:__("hangup_button_title")})),
|
||||
React.DOM.li(null, MediaControlButton( {action:this.handleToggleVideo,
|
||||
enabled:this.props.video.enabled,
|
||||
scope:"local", type:"video"} )),
|
||||
React.DOM.li(null, MediaControlButton( {action:this.handleToggleAudio,
|
||||
enabled:this.props.audio.enabled,
|
||||
scope:"local", type:"audio"} ))
|
||||
)
|
||||
);
|
||||
/* jshint ignore:end */
|
||||
@ -335,16 +336,16 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
render: function() {
|
||||
/* jshint ignore:start */
|
||||
return (
|
||||
React.DOM.div({className: "conversation"},
|
||||
ConversationToolbar({video: this.state.video,
|
||||
audio: this.state.audio,
|
||||
publishStream: this.publishStream,
|
||||
hangup: this.hangup}),
|
||||
React.DOM.div({className: "media nested"},
|
||||
React.DOM.div({className: "video_wrapper remote_wrapper"},
|
||||
React.DOM.div({className: "video_inner remote"})
|
||||
),
|
||||
React.DOM.div({className: "local"})
|
||||
React.DOM.div( {className:"conversation"},
|
||||
ConversationToolbar( {video:this.state.video,
|
||||
audio:this.state.audio,
|
||||
publishStream:this.publishStream,
|
||||
hangup:this.hangup} ),
|
||||
React.DOM.div( {className:"media nested"},
|
||||
React.DOM.div( {className:"video_wrapper remote_wrapper"},
|
||||
React.DOM.div( {className:"video_inner remote"})
|
||||
),
|
||||
React.DOM.div( {className:"local"})
|
||||
)
|
||||
)
|
||||
);
|
||||
@ -352,6 +353,230 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback outer layout.
|
||||
*
|
||||
* Props:
|
||||
* -
|
||||
*/
|
||||
var FeedbackLayout = React.createClass({displayName: 'FeedbackLayout',
|
||||
propTypes: {
|
||||
children: React.PropTypes.component.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var backButton = React.DOM.div(null );
|
||||
if (this.props.reset) {
|
||||
backButton = (
|
||||
React.DOM.button( {className:"back", type:"button", onClick:this.props.reset},
|
||||
"« ",__("feedback_back_button")
|
||||
)
|
||||
);
|
||||
}
|
||||
return (
|
||||
React.DOM.div( {className:"feedback"},
|
||||
backButton,
|
||||
React.DOM.h3(null, this.props.title),
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Detailed feedback form.
|
||||
*/
|
||||
var FeedbackForm = React.createClass({displayName: 'FeedbackForm',
|
||||
propTypes: {
|
||||
pending: React.PropTypes.bool,
|
||||
sendFeedback: React.PropTypes.func,
|
||||
reset: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {reason: "", custom: ""};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {pending: false};
|
||||
},
|
||||
|
||||
handleReasonChange: function(event) {
|
||||
var reason = event.target.value;
|
||||
if (reason === "other") {
|
||||
this.setState({reason: reason});
|
||||
} else {
|
||||
// resets custom text field
|
||||
this.setState({custom: ""});
|
||||
}
|
||||
},
|
||||
|
||||
handleCustomTextChange: function(event) {
|
||||
this.setState({custom: event.target.value});
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
var reason = this.refs.reason.getDOMNode().value.trim();
|
||||
var custom = this.refs.custom.getDOMNode().value.trim();
|
||||
this.props.sendFeedback({
|
||||
reason: reason,
|
||||
custom: reason === "other" ? custom : ""
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
FeedbackLayout( {title:__("feedback_what_makes_you_sad"),
|
||||
reset:this.props.reset},
|
||||
React.DOM.form( {onSubmit:this.handleFormSubmit},
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason",
|
||||
value:"audio_quality", onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_audio_quality")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason",
|
||||
value:"video_quality", onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_video_quality")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason",
|
||||
value:"disconnected", onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_was_disconnected")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason", value:"confusing",
|
||||
onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_confusing")
|
||||
),
|
||||
React.DOM.label(null,
|
||||
React.DOM.input( {type:"radio", ref:"reason", name:"reason", value:"other",
|
||||
onChange:this.handleReasonChange} ),
|
||||
__("feedback_reason_other")
|
||||
),
|
||||
React.DOM.p(null, React.DOM.input( {type:"text", ref:"custom", name:"custom",
|
||||
disabled:this.state.reason !== "other",
|
||||
onChange:this.handleCustomTextChange,
|
||||
value:this.state.custom} )),
|
||||
React.DOM.button( {type:"submit", className:"btn btn-success",
|
||||
disabled:this.props.pending},
|
||||
__("feedback_submit_button")
|
||||
)
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback received view.
|
||||
*/
|
||||
var FeedbackReceived = React.createClass({displayName: 'FeedbackReceived',
|
||||
getInitialState: function() {
|
||||
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._timer = setInterval(function() {
|
||||
this.setState({countdown: this.state.countdown - 1});
|
||||
}.bind(this), 1000);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.countdown < 1) {
|
||||
clearInterval(this._timer);
|
||||
window.close();
|
||||
}
|
||||
return (
|
||||
FeedbackLayout( {title:__("feedback_thank_you_heading")},
|
||||
React.DOM.p( {className:"info thank-you"}, __("feedback_window_will_close_in", {
|
||||
countdown: this.state.countdown
|
||||
}))
|
||||
)
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback view.
|
||||
*/
|
||||
var FeedbackView = React.createClass({displayName: 'FeedbackView',
|
||||
propTypes: {
|
||||
// A loop.FeedbackAPIClient instance
|
||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
||||
// The current feedback submission flow step name
|
||||
step: React.PropTypes.oneOf(["start", "form", "finished"])
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {pending: false, step: this.props.step || "start"};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {step: "start"};
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
|
||||
handleHappyClick: function() {
|
||||
this.setState({step: "finished"});
|
||||
},
|
||||
|
||||
handleSadClick: function() {
|
||||
this.setState({step: "form"});
|
||||
},
|
||||
|
||||
sendFeedback: function(fields) {
|
||||
// Setting state.pending to true will disable the submit button to avoid
|
||||
// multiple submissions
|
||||
this.setState({pending: true});
|
||||
// Sends feedback data
|
||||
this.props.feedbackApiClient.send(fields, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
_onFeedbackSent: function(err) {
|
||||
if (err) {
|
||||
// XXX better end user error reporting, see bug 1046738
|
||||
console.error("Unable to send user feedback", err);
|
||||
}
|
||||
this.setState({pending: false, step: "finished"});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
switch(this.state.step) {
|
||||
case "finished":
|
||||
return FeedbackReceived(null );
|
||||
case "form":
|
||||
return FeedbackForm( {feedbackApiClient:this.props.feedbackApiClient,
|
||||
sendFeedback:this.sendFeedback,
|
||||
reset:this.reset,
|
||||
pending:this.state.pending} );
|
||||
default:
|
||||
return (
|
||||
FeedbackLayout( {title:__("feedback_call_experience_heading")},
|
||||
React.DOM.div( {className:"faces"},
|
||||
React.DOM.button( {className:"face face-happy",
|
||||
onClick:this.handleHappyClick}),
|
||||
React.DOM.button( {className:"face face-sad",
|
||||
onClick:this.handleSadClick})
|
||||
)
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
@ -518,6 +743,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
BaseView: BaseView,
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
FeedbackView: FeedbackView,
|
||||
MediaControlButton: MediaControlButton,
|
||||
NotificationListView: NotificationListView,
|
||||
NotificationView: NotificationView,
|
||||
|
@ -13,6 +13,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
|
||||
var sharedModels = loop.shared.models;
|
||||
var __ = l10n.get;
|
||||
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
||||
|
||||
/**
|
||||
* L10n view. Translates resulting view DOM fragment once rendered.
|
||||
@ -352,6 +353,230 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback outer layout.
|
||||
*
|
||||
* Props:
|
||||
* -
|
||||
*/
|
||||
var FeedbackLayout = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.component.isRequired,
|
||||
title: React.PropTypes.string.isRequired,
|
||||
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var backButton = <div />;
|
||||
if (this.props.reset) {
|
||||
backButton = (
|
||||
<button className="back" type="button" onClick={this.props.reset}>
|
||||
« {__("feedback_back_button")}
|
||||
</button>
|
||||
);
|
||||
}
|
||||
return (
|
||||
<div className="feedback">
|
||||
{backButton}
|
||||
<h3>{this.props.title}</h3>
|
||||
{this.props.children}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Detailed feedback form.
|
||||
*/
|
||||
var FeedbackForm = React.createClass({
|
||||
propTypes: {
|
||||
pending: React.PropTypes.bool,
|
||||
sendFeedback: React.PropTypes.func,
|
||||
reset: React.PropTypes.func
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {reason: "", custom: ""};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {pending: false};
|
||||
},
|
||||
|
||||
handleReasonChange: function(event) {
|
||||
var reason = event.target.value;
|
||||
if (reason === "other") {
|
||||
this.setState({reason: reason});
|
||||
} else {
|
||||
// resets custom text field
|
||||
this.setState({custom: ""});
|
||||
}
|
||||
},
|
||||
|
||||
handleCustomTextChange: function(event) {
|
||||
this.setState({custom: event.target.value});
|
||||
},
|
||||
|
||||
handleFormSubmit: function(event) {
|
||||
event.preventDefault();
|
||||
var reason = this.refs.reason.getDOMNode().value.trim();
|
||||
var custom = this.refs.custom.getDOMNode().value.trim();
|
||||
this.props.sendFeedback({
|
||||
reason: reason,
|
||||
custom: reason === "other" ? custom : ""
|
||||
});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
return (
|
||||
<FeedbackLayout title={__("feedback_what_makes_you_sad")}
|
||||
reset={this.props.reset}>
|
||||
<form onSubmit={this.handleFormSubmit}>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason"
|
||||
value="audio_quality" onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_audio_quality")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason"
|
||||
value="video_quality" onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_video_quality")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason"
|
||||
value="disconnected" onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_was_disconnected")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason" value="confusing"
|
||||
onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_confusing")}
|
||||
</label>
|
||||
<label>
|
||||
<input type="radio" ref="reason" name="reason" value="other"
|
||||
onChange={this.handleReasonChange} />
|
||||
{__("feedback_reason_other")}
|
||||
</label>
|
||||
<p><input type="text" ref="custom" name="custom"
|
||||
disabled={this.state.reason !== "other"}
|
||||
onChange={this.handleCustomTextChange}
|
||||
value={this.state.custom} /></p>
|
||||
<button type="submit" className="btn btn-success"
|
||||
disabled={this.props.pending}>
|
||||
{__("feedback_submit_button")}
|
||||
</button>
|
||||
</form>
|
||||
</FeedbackLayout>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback received view.
|
||||
*/
|
||||
var FeedbackReceived = React.createClass({
|
||||
getInitialState: function() {
|
||||
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
this._timer = setInterval(function() {
|
||||
this.setState({countdown: this.state.countdown - 1});
|
||||
}.bind(this), 1000);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
if (this._timer) {
|
||||
clearInterval(this._timer);
|
||||
}
|
||||
},
|
||||
|
||||
render: function() {
|
||||
if (this.state.countdown < 1) {
|
||||
clearInterval(this._timer);
|
||||
window.close();
|
||||
}
|
||||
return (
|
||||
<FeedbackLayout title={__("feedback_thank_you_heading")}>
|
||||
<p className="info thank-you">{__("feedback_window_will_close_in", {
|
||||
countdown: this.state.countdown
|
||||
})}</p>
|
||||
</FeedbackLayout>
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Feedback view.
|
||||
*/
|
||||
var FeedbackView = React.createClass({
|
||||
propTypes: {
|
||||
// A loop.FeedbackAPIClient instance
|
||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
||||
// The current feedback submission flow step name
|
||||
step: React.PropTypes.oneOf(["start", "form", "finished"])
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {pending: false, step: this.props.step || "start"};
|
||||
},
|
||||
|
||||
getInitialProps: function() {
|
||||
return {step: "start"};
|
||||
},
|
||||
|
||||
reset: function() {
|
||||
this.setState(this.getInitialState());
|
||||
},
|
||||
|
||||
handleHappyClick: function() {
|
||||
this.setState({step: "finished"});
|
||||
},
|
||||
|
||||
handleSadClick: function() {
|
||||
this.setState({step: "form"});
|
||||
},
|
||||
|
||||
sendFeedback: function(fields) {
|
||||
// Setting state.pending to true will disable the submit button to avoid
|
||||
// multiple submissions
|
||||
this.setState({pending: true});
|
||||
// Sends feedback data
|
||||
this.props.feedbackApiClient.send(fields, this._onFeedbackSent);
|
||||
},
|
||||
|
||||
_onFeedbackSent: function(err) {
|
||||
if (err) {
|
||||
// XXX better end user error reporting, see bug 1046738
|
||||
console.error("Unable to send user feedback", err);
|
||||
}
|
||||
this.setState({pending: false, step: "finished"});
|
||||
},
|
||||
|
||||
render: function() {
|
||||
switch(this.state.step) {
|
||||
case "finished":
|
||||
return <FeedbackReceived />;
|
||||
case "form":
|
||||
return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
|
||||
sendFeedback={this.sendFeedback}
|
||||
reset={this.reset}
|
||||
pending={this.state.pending} />;
|
||||
default:
|
||||
return (
|
||||
<FeedbackLayout title={__("feedback_call_experience_heading")}>
|
||||
<div className="faces">
|
||||
<button className="face face-happy"
|
||||
onClick={this.handleHappyClick}></button>
|
||||
<button className="face face-sad"
|
||||
onClick={this.handleSadClick}></button>
|
||||
</div>
|
||||
</FeedbackLayout>
|
||||
);
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Notification view.
|
||||
*/
|
||||
@ -518,6 +743,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||
BaseView: BaseView,
|
||||
ConversationView: ConversationView,
|
||||
ConversationToolbar: ConversationToolbar,
|
||||
FeedbackView: FeedbackView,
|
||||
MediaControlButton: MediaControlButton,
|
||||
NotificationListView: NotificationListView,
|
||||
NotificationView: NotificationView,
|
||||
|
@ -22,6 +22,8 @@ browser.jar:
|
||||
content/browser/loop/shared/css/conversation.css (content/shared/css/conversation.css)
|
||||
|
||||
# Shared images
|
||||
content/browser/loop/shared/img/happy.png (content/shared/img/happy.png)
|
||||
content/browser/loop/shared/img/sad.png (content/shared/img/sad.png)
|
||||
content/browser/loop/shared/img/icon_32.png (content/shared/img/icon_32.png)
|
||||
content/browser/loop/shared/img/icon_64.png (content/shared/img/icon_64.png)
|
||||
content/browser/loop/shared/img/loading-icon.gif (content/shared/img/loading-icon.gif)
|
||||
|
@ -42,14 +42,14 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
|
||||
render: function() {
|
||||
if (this.props.helper.isFirefox(navigator.userAgent)) {
|
||||
return React.DOM.div(null);
|
||||
return React.DOM.div(null );
|
||||
}
|
||||
return (
|
||||
React.DOM.div({className: "promote-firefox"},
|
||||
React.DOM.h3(null, __("promote_firefox_hello_heading")),
|
||||
React.DOM.div( {className:"promote-firefox"},
|
||||
React.DOM.h3(null, __("promote_firefox_hello_heading")),
|
||||
React.DOM.p(null,
|
||||
React.DOM.a({className: "btn btn-large btn-success",
|
||||
href: "https://www.mozilla.org/firefox/"},
|
||||
React.DOM.a( {className:"btn btn-large btn-success",
|
||||
href:"https://www.mozilla.org/firefox/"},
|
||||
__("get_firefox_button")
|
||||
)
|
||||
)
|
||||
@ -69,13 +69,13 @@ loop.webapp = (function($, _, OT, webL10n) {
|
||||
render: function() {
|
||||
/* jshint ignore:start */
|
||||
return (
|
||||
React.DOM.div({className: "expired-url-info"},
|
||||
React.DOM.div({className: "info-panel"},
|
||||
React.DOM.div({className: "firefox-logo"}),
|
||||
React.DOM.h1(null, __("call_url_unavailable_notification_heading")),
|
||||
React.DOM.div( {className:"expired-url-info"},
|
||||
React.DOM.div( {className:"info-panel"},
|
||||
React.DOM.div( {className:"firefox-logo"} ),
|
||||
React.DOM.h1(null, __("call_url_unavailable_notification_heading")),
|
||||
React.DOM.h4(null, __("call_url_unavailable_notification_message"))
|
||||
),
|
||||
PromoteFirefoxView({helper: this.props.helper})
|
||||
),
|
||||
PromoteFirefoxView( {helper:this.props.helper} )
|
||||
)
|
||||
);
|
||||
/* jshint ignore:end */
|
||||
|
@ -248,10 +248,23 @@ describe("loop.conversation", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#ended", function() {
|
||||
describe("#feedback", function() {
|
||||
beforeEach(function() {
|
||||
sandbox.stub(router, "loadReactComponent");
|
||||
});
|
||||
|
||||
// XXX When the call is ended gracefully, we should check that we
|
||||
// close connections nicely
|
||||
it("should close the window");
|
||||
// close connections nicely (see bug 1046744)
|
||||
it("should display a feedback form view", function() {
|
||||
router.feedback();
|
||||
|
||||
sinon.assert.calledOnce(router.loadReactComponent);
|
||||
sinon.assert.calledWith(router.loadReactComponent,
|
||||
sinon.match(function(value) {
|
||||
return TestUtils.isComponentOfType(value,
|
||||
loop.shared.views.FeedbackView);
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe("#blocked", function() {
|
||||
@ -319,44 +332,31 @@ describe("loop.conversation", function() {
|
||||
sinon.assert.calledWith(router.navigate, "call/ongoing");
|
||||
});
|
||||
|
||||
it("should navigate to call/ended when the call session ends",
|
||||
it("should navigate to call/feedback when the call session ends",
|
||||
function() {
|
||||
conversation.trigger("session:ended");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
sinon.assert.calledWith(router.navigate, "call/feedback");
|
||||
});
|
||||
|
||||
it("should navigate to call/ended when peer hangs up", function() {
|
||||
it("should navigate to call/feedback when peer hangs up", function() {
|
||||
conversation.trigger("session:peer-hungup");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
sinon.assert.calledWith(router.navigate, "call/feedback");
|
||||
});
|
||||
|
||||
it("should navigate to call/{token} when network disconnects",
|
||||
it("should navigate to call/feedback when network disconnects",
|
||||
function() {
|
||||
conversation.trigger("session:network-disconnected");
|
||||
|
||||
sinon.assert.calledOnce(router.navigate);
|
||||
sinon.assert.calledWith(router.navigate, "call/ended");
|
||||
sinon.assert.calledWith(router.navigate, "call/feedback");
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("EndedCallView", function() {
|
||||
describe("#closeWindow", function() {
|
||||
it("should close the conversation window", function() {
|
||||
sandbox.stub(window, "close");
|
||||
var view = new loop.conversation.EndedCallView();
|
||||
|
||||
view.closeWindow({preventDefault: sandbox.spy()});
|
||||
|
||||
sinon.assert.calledOnce(window.close);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("IncomingCallView", function() {
|
||||
var view, model;
|
||||
|
||||
|
@ -401,6 +401,89 @@ describe("loop.shared.views", function() {
|
||||
});
|
||||
});
|
||||
|
||||
describe("FeedbackView", function() {
|
||||
var comp, fakeFeedbackApiClient;
|
||||
|
||||
beforeEach(function() {
|
||||
fakeFeedbackApiClient = {send: sandbox.stub()};
|
||||
comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
|
||||
feedbackApiClient: fakeFeedbackApiClient
|
||||
}));
|
||||
});
|
||||
|
||||
it("should thank the user for feedback when clicking on the happy face",
|
||||
function() {
|
||||
var happyFace = comp.getDOMNode().querySelector(".face-happy");
|
||||
|
||||
TestUtils.Simulate.click(happyFace);
|
||||
|
||||
expect(comp.getDOMNode()
|
||||
.querySelectorAll(".feedback .thank-you").length).eql(1);
|
||||
expect(comp.getDOMNode().querySelector("button.back")).to.be.a("null");
|
||||
});
|
||||
|
||||
it("should bring the user to feedback form when clicking on the sad face",
|
||||
function() {
|
||||
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
||||
|
||||
TestUtils.Simulate.click(sadFace);
|
||||
|
||||
expect(comp.getDOMNode().querySelectorAll("form").length).eql(1);
|
||||
});
|
||||
|
||||
it("should disable the form submit button once the form is first submitted",
|
||||
function() {
|
||||
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
||||
TestUtils.Simulate.click(sadFace);
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='confusing']"), {
|
||||
target: {checked: true}
|
||||
});
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
|
||||
// see https://bugzilla.mozilla.org/show_bug.cgi?id=1033841#c10
|
||||
expect(comp.getDOMNode()
|
||||
.querySelector("form button")
|
||||
.getAttribute("disabled")).eql("true");
|
||||
});
|
||||
|
||||
it("should send feedback data over the feedback API client", function() {
|
||||
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".face-sad"));
|
||||
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='other']"), {
|
||||
target: {checked: true}
|
||||
});
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[name='custom']"), {
|
||||
target: {value: "fake"}
|
||||
});
|
||||
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
|
||||
sinon.assert.calledOnce(fakeFeedbackApiClient.send);
|
||||
sinon.assert.calledWith(fakeFeedbackApiClient.send, {
|
||||
reason: "other",
|
||||
custom: "fake"
|
||||
});
|
||||
});
|
||||
|
||||
it("should thank the user when feedback data has been sent", function() {
|
||||
fakeFeedbackApiClient.send = function(data, cb) {
|
||||
cb();
|
||||
};
|
||||
TestUtils.Simulate.click(comp.getDOMNode().querySelector(".face-sad"));
|
||||
TestUtils.Simulate.change(
|
||||
comp.getDOMNode().querySelector("[value='confusing']"), {
|
||||
target: {checked: true}
|
||||
});
|
||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
||||
|
||||
expect(comp.getDOMNode()
|
||||
.querySelectorAll(".feedback .thank-you").length).eql(1);
|
||||
});
|
||||
});
|
||||
|
||||
describe("NotificationView", function() {
|
||||
var collection, model, view;
|
||||
|
||||
|
@ -22,6 +22,7 @@
|
||||
// 3. Shared components
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var ConversationView = loop.shared.views.ConversationView;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
|
||||
// Local helpers
|
||||
function returnTrue() {
|
||||
@ -32,14 +33,22 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// local fakes
|
||||
var fakeFeedbackApiClient = {
|
||||
send: function(fields, cb) {
|
||||
alert("Sent feedback data: " + JSON.stringify(fields));
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
var Example = React.createClass({displayName: 'Example',
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
return (
|
||||
React.DOM.div({className: "example"},
|
||||
React.DOM.h3(null, this.props.summary),
|
||||
React.DOM.div({className: cx({comp: true, dashed: this.props.dashed}),
|
||||
style: this.props.style || {}},
|
||||
React.DOM.div( {className:"example"},
|
||||
React.DOM.h3(null, this.props.summary),
|
||||
React.DOM.div( {className:cx({comp: true, dashed: this.props.dashed}),
|
||||
style:this.props.style || {}},
|
||||
this.props.children
|
||||
)
|
||||
)
|
||||
@ -50,8 +59,8 @@
|
||||
var Section = React.createClass({displayName: 'Section',
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.section({id: this.props.name},
|
||||
React.DOM.h1(null, this.props.name),
|
||||
React.DOM.section( {id:this.props.name},
|
||||
React.DOM.h1(null, this.props.name),
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
@ -61,19 +70,19 @@
|
||||
var ShowCase = React.createClass({displayName: 'ShowCase',
|
||||
render: function() {
|
||||
return (
|
||||
React.DOM.div({className: "showcase"},
|
||||
React.DOM.div( {className:"showcase"},
|
||||
React.DOM.header(null,
|
||||
React.DOM.h1(null, "Loop UI Components Showcase"),
|
||||
React.DOM.nav({className: "menu"},
|
||||
React.DOM.h1(null, "Loop UI Components Showcase"),
|
||||
React.DOM.nav( {className:"menu"},
|
||||
React.Children.map(this.props.children, function(section) {
|
||||
return (
|
||||
React.DOM.a({className: "btn btn-info", href: "#" + section.props.name},
|
||||
React.DOM.a( {className:"btn btn-info", href:"#" + section.props.name},
|
||||
section.props.name
|
||||
)
|
||||
);
|
||||
})
|
||||
)
|
||||
),
|
||||
),
|
||||
this.props.children
|
||||
)
|
||||
);
|
||||
@ -84,42 +93,54 @@
|
||||
render: function() {
|
||||
return (
|
||||
ShowCase(null,
|
||||
Section({name: "PanelView"},
|
||||
Example({summary: "332px wide", dashed: "true", style: {width: "332px"}},
|
||||
PanelView(null)
|
||||
Section( {name:"PanelView"},
|
||||
Example( {summary:"332px wide", dashed:"true", style:{width: "332px"}},
|
||||
PanelView(null )
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
Section({name: "IncomingCallView"},
|
||||
Example({summary: "Default", dashed: "true", style: {width: "280px"}},
|
||||
IncomingCallView(null)
|
||||
Section( {name:"IncomingCallView"},
|
||||
Example( {summary:"Default", dashed:"true", style:{width: "280px"}},
|
||||
IncomingCallView(null )
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
Section({name: "ConversationToolbar"},
|
||||
Example({summary: "Default"},
|
||||
ConversationToolbar({video: {enabled: true}, audio: {enabled: true}})
|
||||
),
|
||||
Example({summary: "Video muted"},
|
||||
ConversationToolbar({video: {enabled: false}, audio: {enabled: true}})
|
||||
),
|
||||
Example({summary: "Audio muted"},
|
||||
ConversationToolbar({video: {enabled: true}, audio: {enabled: false}})
|
||||
Section( {name:"ConversationToolbar"},
|
||||
Example( {summary:"Default"},
|
||||
ConversationToolbar( {video:{enabled: true}, audio:{enabled: true}} )
|
||||
),
|
||||
Example( {summary:"Video muted"},
|
||||
ConversationToolbar( {video:{enabled: false}, audio:{enabled: true}} )
|
||||
),
|
||||
Example( {summary:"Audio muted"},
|
||||
ConversationToolbar( {video:{enabled: true}, audio:{enabled: false}} )
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
Section({name: "ConversationView"},
|
||||
Example({summary: "Default"},
|
||||
ConversationView({video: {enabled: true}, audio: {enabled: true}})
|
||||
Section( {name:"ConversationView"},
|
||||
Example( {summary:"Default"},
|
||||
ConversationView( {video:{enabled: true}, audio:{enabled: true}} )
|
||||
)
|
||||
),
|
||||
),
|
||||
|
||||
Section({name: "CallUrlExpiredView"},
|
||||
Example({summary: "Firefox User"},
|
||||
CallUrlExpiredView({helper: {isFirefox: returnTrue}})
|
||||
),
|
||||
Example({summary: "Non-Firefox User"},
|
||||
CallUrlExpiredView({helper: {isFirefox: returnFalse}})
|
||||
Section( {name:"FeedbackView"},
|
||||
Example( {summary:"Default (useable demo)", dashed:"true", style:{width: "280px"}},
|
||||
FeedbackView( {feedbackApiClient:fakeFeedbackApiClient} )
|
||||
),
|
||||
Example( {summary:"Detailed form", dashed:"true", style:{width: "280px"}},
|
||||
FeedbackView( {step:"form"} )
|
||||
),
|
||||
Example( {summary:"Thank you!", dashed:"true", style:{width: "280px"}},
|
||||
FeedbackView( {step:"finished"} )
|
||||
)
|
||||
),
|
||||
|
||||
Section( {name:"CallUrlExpiredView"},
|
||||
Example( {summary:"Firefox User"},
|
||||
CallUrlExpiredView( {helper:{isFirefox: returnTrue}} )
|
||||
),
|
||||
Example( {summary:"Non-Firefox User"},
|
||||
CallUrlExpiredView( {helper:{isFirefox: returnFalse}} )
|
||||
)
|
||||
)
|
||||
)
|
||||
@ -128,6 +149,6 @@
|
||||
});
|
||||
|
||||
window.addEventListener("DOMContentLoaded", function() {
|
||||
React.renderComponent(App(null), document.body);
|
||||
React.renderComponent(App(null ), document.body);
|
||||
});
|
||||
})();
|
||||
|
@ -22,6 +22,7 @@
|
||||
// 3. Shared components
|
||||
var ConversationToolbar = loop.shared.views.ConversationToolbar;
|
||||
var ConversationView = loop.shared.views.ConversationView;
|
||||
var FeedbackView = loop.shared.views.FeedbackView;
|
||||
|
||||
// Local helpers
|
||||
function returnTrue() {
|
||||
@ -32,6 +33,14 @@
|
||||
return false;
|
||||
}
|
||||
|
||||
// local fakes
|
||||
var fakeFeedbackApiClient = {
|
||||
send: function(fields, cb) {
|
||||
alert("Sent feedback data: " + JSON.stringify(fields));
|
||||
cb();
|
||||
}
|
||||
};
|
||||
|
||||
var Example = React.createClass({
|
||||
render: function() {
|
||||
var cx = React.addons.classSet;
|
||||
@ -114,6 +123,18 @@
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="FeedbackView">
|
||||
<Example summary="Default (useable demo)" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView feedbackApiClient={fakeFeedbackApiClient} />
|
||||
</Example>
|
||||
<Example summary="Detailed form" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView step="form" />
|
||||
</Example>
|
||||
<Example summary="Thank you!" dashed="true" style={{width: "280px"}}>
|
||||
<FeedbackView step="finished" />
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="CallUrlExpiredView">
|
||||
<Example summary="Firefox User">
|
||||
<CallUrlExpiredView helper={{isFirefox: returnTrue}} />
|
||||
|
@ -28,7 +28,6 @@ unmute_local_video_button_title=Unmute your video
|
||||
|
||||
peer_ended_conversation=Your peer ended the conversation.
|
||||
call_has_ended=Your call has ended.
|
||||
close_window=Close this window
|
||||
|
||||
cannot_start_call_session_not_ready=Can't start call, session is not ready.
|
||||
network_disconnected=The network connection terminated abruptly.
|
||||
@ -39,3 +38,17 @@ connection_error_see_console_notification=Call failed; see console for details.
|
||||
legal_text_and_links=By using this product you agree to the <a \
|
||||
target="_blank" href="{{terms_of_use_url}}">Terms of Use</a> and <a \
|
||||
href="{{privacy_notice_url}}">Privacy Notice</a>.
|
||||
|
||||
feedback_call_experience_heading=How was your call experience?
|
||||
feedback_what_makes_you_sad=What makes you sad?
|
||||
feedback_thank_you_heading=Thank you for your feedback!
|
||||
feedback_reason_audio_quality=Audio quality
|
||||
feedback_reason_video_quality=Video quality
|
||||
feedback_reason_was_disconnected=Was disconnected
|
||||
feedback_reason_confusing=Confusing
|
||||
feedback_reason_other=Other:
|
||||
feedback_submit_button=Submit
|
||||
feedback_back_button=Back
|
||||
## LOCALIZATION NOTE (feedback_window_will_close_in): In this item, don't
|
||||
## translate the part between {{..}}
|
||||
feedback_window_will_close_in=This window will close in {{countdown}} seconds
|
||||
|
Loading…
Reference in New Issue
Block a user