fune/browser/components/loop/content/shared/js/feedbackViews.js

306 lines
9.4 KiB
JavaScript

/** @jsx React.DOM */
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* jshint newcap:false */
/* global loop:true, React */
var loop = loop || {};
loop.shared = loop.shared || {};
loop.shared.views = loop.shared.views || {};
loop.shared.views.FeedbackView = (function(l10n) {
"use strict";
var sharedActions = loop.shared.actions;
var sharedMixins = loop.shared.mixins;
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS =
loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
/**
* 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.createElement("div", null);
if (this.props.reset) {
backButton = (
React.createElement("button", {className: "fx-embedded-btn-back", type: "button",
onClick: this.props.reset},
"« ", l10n.get("feedback_back_button")
)
);
}
return (
React.createElement("div", {className: "feedback"},
backButton,
React.createElement("h3", null, this.props.title),
this.props.children
)
);
}
});
/**
* Detailed feedback form.
*/
var FeedbackForm = React.createClass({displayName: "FeedbackForm",
propTypes: {
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
pending: React.PropTypes.bool,
reset: React.PropTypes.func
},
getInitialState: function() {
return {category: "", description: ""};
},
getDefaultProps: function() {
return {pending: false};
},
_getCategories: function() {
return {
audio_quality: l10n.get("feedback_category_audio_quality"),
video_quality: l10n.get("feedback_category_video_quality"),
disconnected : l10n.get("feedback_category_was_disconnected"),
confusing: l10n.get("feedback_category_confusing2"),
other: l10n.get("feedback_category_other2")
};
},
_getCategoryFields: function() {
var categories = this._getCategories();
return Object.keys(categories).map(function(category, key) {
return (
React.createElement("label", {key: key, className: "feedback-category-label"},
React.createElement("input", {type: "radio", ref: "category", name: "category",
className: "feedback-category-radio",
value: category,
onChange: this.handleCategoryChange,
checked: this.state.category === category}),
categories[category]
)
);
}, this);
},
/**
* Checks if the form is ready for submission:
*
* - no feedback submission should be pending.
* - a category (reason) must be chosen;
* - if the "other" category is chosen, a custom description must have been
* entered by the end user;
*
* @return {Boolean}
*/
_isFormReady: function() {
if (this.props.pending || !this.state.category) {
return false;
}
if (this.state.category === "other" && !this.state.description) {
return false;
}
return true;
},
handleCategoryChange: function(event) {
var category = event.target.value;
this.setState({
category: category
});
if (category == "other") {
this.refs.description.getDOMNode().focus();
}
},
handleDescriptionFieldChange: function(event) {
this.setState({description: event.target.value});
},
handleFormSubmit: function(event) {
event.preventDefault();
// XXX this feels ugly, we really want a feedbackActions object here.
this.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
happy: false,
category: this.state.category,
description: this.state.description
}));
},
render: function() {
return (
React.createElement(FeedbackLayout, {title: l10n.get("feedback_category_list_heading"),
reset: this.props.reset},
React.createElement("form", {onSubmit: this.handleFormSubmit},
this._getCategoryFields(),
React.createElement("p", null,
React.createElement("input", {type: "text", ref: "description", name: "description",
className: "feedback-description",
onChange: this.handleDescriptionFieldChange,
value: this.state.description,
placeholder:
l10n.get("feedback_custom_category_text_placeholder")})
),
React.createElement("button", {type: "submit", className: "btn btn-success",
disabled: !this._isFormReady()},
l10n.get("feedback_submit_button")
)
)
)
);
}
});
/**
* Feedback received view.
*
* Props:
* - {Function} onAfterFeedbackReceived Function to execute after the
* WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
*/
var FeedbackReceived = React.createClass({displayName: "FeedbackReceived",
propTypes: {
onAfterFeedbackReceived: React.PropTypes.func
},
getInitialState: function() {
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
},
componentDidMount: function() {
this._timer = setInterval(function() {
if (this.state.countdown == 1) {
clearInterval(this._timer);
if (this.props.onAfterFeedbackReceived) {
this.props.onAfterFeedbackReceived();
}
return;
}
this.setState({countdown: this.state.countdown - 1});
}.bind(this), 1000);
},
componentWillUnmount: function() {
if (this._timer) {
clearInterval(this._timer);
}
},
render: function() {
return (
React.createElement(FeedbackLayout, {title: l10n.get("feedback_thank_you_heading")},
React.createElement("p", {className: "info thank-you"},
l10n.get("feedback_window_will_close_in2", {
countdown: this.state.countdown,
num: this.state.countdown
}))
)
);
}
});
/**
* Feedback view.
*/
var FeedbackView = React.createClass({displayName: "FeedbackView",
mixins: [
Backbone.Events,
loop.store.StoreMixin("feedbackStore")
],
propTypes: {
onAfterFeedbackReceived: React.PropTypes.func,
// Used by the UI showcase.
feedbackState: React.PropTypes.string
},
getInitialState: function() {
var storeState = this.getStoreState();
return _.extend({}, storeState, {
feedbackState: this.props.feedbackState || storeState.feedbackState
});
},
reset: function() {
this.setState(this.getStore().getInitialStoreState());
},
handleHappyClick: function() {
// XXX: If the user is happy, we directly send this information to the
// feedback API; this is a behavior we might want to revisit later.
this.getStore().dispatchAction(new sharedActions.SendFeedback({
happy: true,
category: "",
description: ""
}));
},
handleSadClick: function() {
this.getStore().dispatchAction(
new sharedActions.RequireFeedbackDetails());
},
_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.feedbackState) {
default:
case FEEDBACK_STATES.INIT: {
return (
React.createElement(FeedbackLayout, {title:
l10n.get("feedback_call_experience_heading2")},
React.createElement("div", {className: "faces"},
React.createElement("button", {className: "face face-happy",
onClick: this.handleHappyClick}),
React.createElement("button", {className: "face face-sad",
onClick: this.handleSadClick})
)
)
);
}
case FEEDBACK_STATES.DETAILS: {
return (
React.createElement(FeedbackForm, {
feedbackStore: this.getStore(),
reset: this.reset,
pending: this.state.feedbackState === FEEDBACK_STATES.PENDING})
);
}
case FEEDBACK_STATES.PENDING:
case FEEDBACK_STATES.SENT:
case FEEDBACK_STATES.FAILED: {
if (this.state.error) {
// XXX better end user error reporting, see bug 1046738
console.error("Error encountered while submitting feedback",
this.state.error);
}
return (
React.createElement(FeedbackReceived, {
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
);
}
}
}
});
return FeedbackView;
})(navigator.mozL10n || document.mozL10n);