--- a/browser/app/profile/firefox.js
+++ b/browser/app/profile/firefox.js
@@ -1758,16 +1758,19 @@ pref("loop.ping.interval", 1800000);
pref("loop.ping.timeout", 10000);
pref("loop.feedback.baseUrl", "https://input.mozilla.org/api/v1/feedback");
pref("loop.feedback.product", "Loop");
pref("loop.debug.loglevel", "Error");
pref("loop.debug.dispatcher", false);
pref("loop.debug.websocket", false);
pref("loop.debug.sdk", false);
pref("loop.debug.twoWayMediaTelemetry", false);
+pref("loop.feedback.dateLastSeenSec", 0);
+pref("loop.feedback.periodSec", 15770000); // 6 months.
+pref("loop.feedback.formURL", "http://www.surveygizmo.com/s3/2227372/Firefox-Hello-Product-Survey");
#ifdef DEBUG
pref("loop.CSP", "default-src 'self' about: file: chrome: http://localhost:*; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net http://localhost:* ws://localhost:*; media-src blob:");
#else
pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src * data:; font-src 'none'; connect-src wss://*.tokbox.com https://*.opentok.com https://*.tokbox.com wss://*.mozilla.com https://*.mozilla.org wss://*.mozaws.net; media-src blob:");
#endif
pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto");
pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
pref("loop.fxa_oauth.tokendata", "");
--- a/browser/components/loop/.eslintignore
+++ b/browser/components/loop/.eslintignore
@@ -12,15 +12,15 @@ standalone/node_modules
# Libs we don't need to check
test/shared/vendor
# These are generated react files that we don't need to check
content/js/contacts.js
content/js/conversation.js
content/js/conversationViews.js
content/js/panel.js
content/js/roomViews.js
-content/shared/js/feedbackViews.js
+content/js/feedbackViews.js
content/shared/js/textChatView.js
content/shared/js/views.js
standalone/content/js/fxOSMarketplace.js
standalone/content/js/standaloneRoomViews.js
standalone/content/js/webapp.js
ui/ui-showcase.js
--- a/browser/components/loop/content/conversation.html
+++ b/browser/components/loop/content/conversation.html
@@ -22,29 +22,27 @@
<script type="text/javascript" src="loop/libs/sdk.js"></script>
<script type="text/javascript" src="loop/shared/libs/react-0.12.2.js"></script>
<script type="text/javascript" src="loop/shared/libs/jquery-2.1.4.js"></script>
<script type="text/javascript" src="loop/shared/libs/lodash-3.9.3.js"></script>
<script type="text/javascript" src="loop/shared/libs/backbone-1.2.1.js"></script>
<script type="text/javascript" src="loop/shared/js/utils.js"></script>
<script type="text/javascript" src="loop/shared/js/mixins.js"></script>
- <script type="text/javascript" src="loop/shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="loop/shared/js/actions.js"></script>
<script type="text/javascript" src="loop/shared/js/validate.js"></script>
<script type="text/javascript" src="loop/shared/js/dispatcher.js"></script>
<script type="text/javascript" src="loop/shared/js/otSdkDriver.js"></script>
<script type="text/javascript" src="loop/shared/js/store.js"></script>
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
<script type="text/javascript" src="loop/shared/js/roomStates.js"></script>
<script type="text/javascript" src="loop/shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="loop/shared/js/activeRoomStore.js"></script>
- <script type="text/javascript" src="loop/shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="loop/shared/js/views.js"></script>
- <script type="text/javascript" src="loop/shared/js/feedbackViews.js"></script>
+ <script type="text/javascript" src="loop/js/feedbackViews.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatStore.js"></script>
<script type="text/javascript" src="loop/shared/js/textChatView.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/shared/js/websocket.js"></script>
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
<script type="text/javascript" src="loop/js/client.js"></script>
<script type="text/javascript" src="loop/js/conversationViews.js"></script>
<script type="text/javascript" src="loop/js/roomStore.js"></script>
--- a/browser/components/loop/content/js/conversation.js
+++ b/browser/components/loop/content/js/conversation.js
@@ -1,60 +1,91 @@
/* 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/. */
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 sharedActions = loop.shared.actions;
var CallControllerView = loop.conversationViews.CallControllerView;
- var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+ var FeedbackView = loop.feedbackViews.FeedbackView;
var GenericFailureView = loop.conversationViews.GenericFailureView;
/**
* Master controller view for handling if incoming or outgoing calls are
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({displayName: "AppControllerView",
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
+ sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
},
getInitialState: function() {
return this.getStoreState();
},
+ _renderFeedbackForm: function() {
+ this.setTitle(mozL10n.get("conversation_has_ended"));
+
+ return (React.createElement(FeedbackView, {
+ mozLoop: this.props.mozLoop,
+ onAfterFeedbackReceived: this.closeWindow}));
+ },
+
+ /**
+ * We only show the feedback for once every 6 months, otherwise close
+ * the window.
+ */
+ handleCallTerminated: function() {
+ var delta = new Date() - new Date(this.state.feedbackTimestamp);
+
+ // Show timestamp if feedback period (6 months) passed.
+ // 0 is default value for pref. Always show feedback form on first use.
+ if (this.state.feedbackTimestamp === 0 ||
+ delta >= this.state.feedbackPeriod) {
+ this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+ return;
+ }
+
+ this.closeWindow();
+ },
+
render: function() {
+ if (this.state.showFeedbackForm) {
+ return this._renderFeedbackForm();
+ }
+
switch(this.state.windowType) {
// CallControllerView is used for both.
case "incoming":
case "outgoing": {
return (React.createElement(CallControllerView, {
dispatcher: this.props.dispatcher,
- mozLoop: this.props.mozLoop}));
+ mozLoop: this.props.mozLoop,
+ onCallTerminated: this.handleCallTerminated}));
}
case "room": {
return (React.createElement(DesktopRoomConversationView, {
dispatcher: this.props.dispatcher,
mozLoop: this.props.mozLoop,
+ onCallTerminated: this.handleCallTerminated,
roomStore: this.props.roomStore}));
}
case "failed": {
return React.createElement(GenericFailureView, {cancelCall: this.closeWindow});
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
@@ -97,25 +128,16 @@ loop.conversation = (function(mozL10n) {
dispatcher: dispatcher,
sdk: OT,
mozLoop: navigator.mozLoop
});
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
- var appVersionInfo = navigator.mozLoop.appVersionInfo;
- var feedbackClient = new loop.FeedbackAPIClient(
- navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
- product: navigator.mozLoop.getLoopPref("feedback.product"),
- platform: appVersionInfo.OS,
- channel: appVersionInfo.channel,
- version: appVersionInfo.version
- });
-
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: client,
isDesktop: true,
@@ -126,27 +148,23 @@ loop.conversation = (function(mozL10n) {
isDesktop: true,
mozLoop: navigator.mozLoop,
sdkDriver: sdkDriver
});
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: activeRoomStore
});
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: feedbackClient
- });
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore,
- feedbackStore: feedbackStore,
textChatStore: textChatStore
});
// Obtain the windowId and pass it through
var locationHash = loop.shared.utils.locationData().hash;
var windowId;
var hash = locationHash.match(/#(.*)/);
--- a/browser/components/loop/content/js/conversation.jsx
+++ b/browser/components/loop/content/js/conversation.jsx
@@ -1,60 +1,91 @@
/* 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/. */
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 sharedActions = loop.shared.actions;
var CallControllerView = loop.conversationViews.CallControllerView;
- var CallIdentifierView = loop.conversationViews.CallIdentifierView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
+ var FeedbackView = loop.feedbackViews.FeedbackView;
var GenericFailureView = loop.conversationViews.GenericFailureView;
/**
* Master controller view for handling if incoming or outgoing calls are
* in progress, and hence, which view to display.
*/
var AppControllerView = React.createClass({
mixins: [
Backbone.Events,
loop.store.StoreMixin("conversationAppStore"),
+ sharedMixins.DocumentTitleMixin,
sharedMixins.WindowCloseMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
},
getInitialState: function() {
return this.getStoreState();
},
+ _renderFeedbackForm: function() {
+ this.setTitle(mozL10n.get("conversation_has_ended"));
+
+ return (<FeedbackView
+ mozLoop={this.props.mozLoop}
+ onAfterFeedbackReceived={this.closeWindow} />);
+ },
+
+ /**
+ * We only show the feedback for once every 6 months, otherwise close
+ * the window.
+ */
+ handleCallTerminated: function() {
+ var delta = new Date() - new Date(this.state.feedbackTimestamp);
+
+ // Show timestamp if feedback period (6 months) passed.
+ // 0 is default value for pref. Always show feedback form on first use.
+ if (this.state.feedbackTimestamp === 0 ||
+ delta >= this.state.feedbackPeriod) {
+ this.props.dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+ return;
+ }
+
+ this.closeWindow();
+ },
+
render: function() {
+ if (this.state.showFeedbackForm) {
+ return this._renderFeedbackForm();
+ }
+
switch(this.state.windowType) {
// CallControllerView is used for both.
case "incoming":
case "outgoing": {
return (<CallControllerView
dispatcher={this.props.dispatcher}
- mozLoop={this.props.mozLoop} />);
+ mozLoop={this.props.mozLoop}
+ onCallTerminated={this.handleCallTerminated} />);
}
case "room": {
return (<DesktopRoomConversationView
dispatcher={this.props.dispatcher}
mozLoop={this.props.mozLoop}
+ onCallTerminated={this.handleCallTerminated}
roomStore={this.props.roomStore} />);
}
case "failed": {
return <GenericFailureView cancelCall={this.closeWindow} />;
}
default: {
// If we don't have a windowType, we don't know what we are yet,
// so don't display anything.
@@ -97,25 +128,16 @@ loop.conversation = (function(mozL10n) {
dispatcher: dispatcher,
sdk: OT,
mozLoop: navigator.mozLoop
});
// expose for functional tests
loop.conversation._sdkDriver = sdkDriver;
- var appVersionInfo = navigator.mozLoop.appVersionInfo;
- var feedbackClient = new loop.FeedbackAPIClient(
- navigator.mozLoop.getLoopPref("feedback.baseUrl"), {
- product: navigator.mozLoop.getLoopPref("feedback.product"),
- platform: appVersionInfo.OS,
- channel: appVersionInfo.channel,
- version: appVersionInfo.version
- });
-
// Create the stores.
var conversationAppStore = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
});
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: client,
isDesktop: true,
@@ -126,27 +148,23 @@ loop.conversation = (function(mozL10n) {
isDesktop: true,
mozLoop: navigator.mozLoop,
sdkDriver: sdkDriver
});
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: activeRoomStore
});
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: feedbackClient
- });
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
loop.store.StoreMixin.register({
conversationAppStore: conversationAppStore,
conversationStore: conversationStore,
- feedbackStore: feedbackStore,
textChatStore: textChatStore
});
// Obtain the windowId and pass it through
var locationHash = loop.shared.utils.locationData().hash;
var windowId;
var hash = locationHash.match(/#(.*)/);
--- a/browser/components/loop/content/js/conversationAppStore.js
+++ b/browser/components/loop/content/js/conversationAppStore.js
@@ -22,24 +22,36 @@ loop.store.ConversationAppStore = (funct
throw new Error("Missing option dispatcher");
}
if (!options.mozLoop) {
throw new Error("Missing option mozLoop");
}
this._dispatcher = options.dispatcher;
this._mozLoop = options.mozLoop;
- this._storeState = {};
+ this._storeState = this.getInitialStoreState();
this._dispatcher.register(this, [
- "getWindowData"
+ "getWindowData",
+ "showFeedbackForm"
]);
};
ConversationAppStore.prototype = _.extend({
+ getInitialStoreState: function() {
+ return {
+ // How often to display the form. Convert seconds to ms.
+ feedbackPeriod: this._mozLoop.getLoopPref("feedback.periodSec") * 1000,
+ // Date when the feedback form was last presented. Convert to ms.
+ feedbackTimestamp: this._mozLoop
+ .getLoopPref("feedback.dateLastSeenSec") * 1000,
+ showFeedbackForm: false
+ };
+ },
+
/**
* Retrieves current store state.
*
* @return {Object}
*/
getStoreState: function() {
return this._storeState;
},
@@ -50,16 +62,30 @@ loop.store.ConversationAppStore = (funct
* @param {Object} state The new store state.
*/
setStoreState: function(state) {
this._storeState = state;
this.trigger("change");
},
/**
+ * Sets store state which will result in the feedback form rendered.
+ * Saves a timestamp of when the feedback was last rendered.
+ */
+ showFeedbackForm: function() {
+ var timestamp = Math.floor(new Date().getTime() / 1000);
+
+ this._mozLoop.setLoopPref("feedback.dateLastSeenSec", timestamp);
+
+ this.setStoreState({
+ showFeedbackForm: true
+ });
+ },
+
+ /**
* Handles the get window data action - obtains the window data,
* updates the store and notifies interested components.
*
* @param {sharedActions.GetWindowData} actionData The action data
*/
getWindowData: function(actionData) {
var windowData = this._mozLoop.getConversationWindowData(actionData.windowId);
--- a/browser/components/loop/content/js/conversationViews.js
+++ b/browser/components/loop/content/js/conversationViews.js
@@ -10,17 +10,16 @@ loop.conversationViews = (function(mozL1
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
- var sharedModels = loop.shared.models;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
// XXXdmose this code is already out of sync with the code in contacts.jsx
// which, unlike this code, now has unit tests. We should totally do the
// above.
@@ -696,17 +695,18 @@ loop.conversationViews = (function(mozL1
sharedMixins.AudioMixin,
sharedMixins.DocumentTitleMixin,
loop.store.StoreMixin("conversationStore"),
Backbone.Events
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
- mozLoop: React.PropTypes.object.isRequired
+ mozLoop: React.PropTypes.object.isRequired,
+ onCallTerminated: React.PropTypes.func.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
_closeWindow: function() {
window.close();
@@ -715,29 +715,16 @@ loop.conversationViews = (function(mozL1
/**
* Returns true if the call is in a cancellable state, during call setup.
*/
_isCancellable: function() {
return this.state.callState !== CALL_STATES.INIT &&
this.state.callState !== CALL_STATES.GATHER;
},
- /**
- * Used to setup and render the feedback view.
- */
- _renderFeedbackView: function() {
- this.setTitle(mozL10n.get("conversation_has_ended"));
-
- return (
- React.createElement(sharedViews.FeedbackView, {
- onAfterFeedbackReceived: this._closeWindow.bind(this)}
- )
- );
- },
-
_renderViewFromCallType: function() {
// For outgoing calls we can display the pending conversation view
// for any state that render() doesn't manage.
if (this.state.outgoing) {
return (React.createElement(PendingConversationView, {
callState: this.state.callState,
contact: this.state.contact,
dispatcher: this.props.dispatcher,
@@ -755,16 +742,24 @@ loop.conversationViews = (function(mozL1
));
}
// Otherwise we're still gathering or connecting, so
// don't display anything.
return null;
},
+ componentDidUpdate: function(prevProps, prevState) {
+ // Handle timestamp and window closing only when the call has terminated.
+ if (prevState.callState === CALL_STATES.ONGOING &&
+ this.state.callState === CALL_STATES.FINISHED) {
+ this.props.onCallTerminated();
+ }
+ },
+
render: function() {
// Set the default title to the contact name or the callerId, note
// that views may override this, e.g. the feedback view.
if (this.state.contact) {
this.setTitle(_getContactDisplayName(this.state.contact));
} else {
this.setTitle(this.state.callerId || "");
}
@@ -787,17 +782,20 @@ loop.conversationViews = (function(mozL1
mediaConnected: this.state.mediaConnected,
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
remoteVideoEnabled: this.state.remoteVideoEnabled,
video: {enabled: !this.state.videoMuted}})
);
}
case CALL_STATES.FINISHED: {
this.play("terminated");
- return this._renderFeedbackView();
+
+ // When conversation ended we either display a feedback form or
+ // close the window. This is decided in the AppControllerView.
+ return null;
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return this._renderViewFromCallType();
}
--- a/browser/components/loop/content/js/conversationViews.jsx
+++ b/browser/components/loop/content/js/conversationViews.jsx
@@ -10,17 +10,16 @@ loop.conversationViews = (function(mozL1
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sharedMixins = loop.shared.mixins;
- var sharedModels = loop.shared.models;
// This duplicates a similar function in contacts.jsx that isn't used in the
// conversation window. If we get too many of these, we might want to consider
// finding a logical place for them to be shared.
// XXXdmose this code is already out of sync with the code in contacts.jsx
// which, unlike this code, now has unit tests. We should totally do the
// above.
@@ -696,17 +695,18 @@ loop.conversationViews = (function(mozL1
sharedMixins.AudioMixin,
sharedMixins.DocumentTitleMixin,
loop.store.StoreMixin("conversationStore"),
Backbone.Events
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
- mozLoop: React.PropTypes.object.isRequired
+ mozLoop: React.PropTypes.object.isRequired,
+ onCallTerminated: React.PropTypes.func.isRequired
},
getInitialState: function() {
return this.getStoreState();
},
_closeWindow: function() {
window.close();
@@ -715,29 +715,16 @@ loop.conversationViews = (function(mozL1
/**
* Returns true if the call is in a cancellable state, during call setup.
*/
_isCancellable: function() {
return this.state.callState !== CALL_STATES.INIT &&
this.state.callState !== CALL_STATES.GATHER;
},
- /**
- * Used to setup and render the feedback view.
- */
- _renderFeedbackView: function() {
- this.setTitle(mozL10n.get("conversation_has_ended"));
-
- return (
- <sharedViews.FeedbackView
- onAfterFeedbackReceived={this._closeWindow.bind(this)}
- />
- );
- },
-
_renderViewFromCallType: function() {
// For outgoing calls we can display the pending conversation view
// for any state that render() doesn't manage.
if (this.state.outgoing) {
return (<PendingConversationView
callState={this.state.callState}
contact={this.state.contact}
dispatcher={this.props.dispatcher}
@@ -755,16 +742,24 @@ loop.conversationViews = (function(mozL1
/>);
}
// Otherwise we're still gathering or connecting, so
// don't display anything.
return null;
},
+ componentDidUpdate: function(prevProps, prevState) {
+ // Handle timestamp and window closing only when the call has terminated.
+ if (prevState.callState === CALL_STATES.ONGOING &&
+ this.state.callState === CALL_STATES.FINISHED) {
+ this.props.onCallTerminated();
+ }
+ },
+
render: function() {
// Set the default title to the contact name or the callerId, note
// that views may override this, e.g. the feedback view.
if (this.state.contact) {
this.setTitle(_getContactDisplayName(this.state.contact));
} else {
this.setTitle(this.state.callerId || "");
}
@@ -787,17 +782,20 @@ loop.conversationViews = (function(mozL1
mediaConnected={this.state.mediaConnected}
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
remoteVideoEnabled={this.state.remoteVideoEnabled}
video={{enabled: !this.state.videoMuted}} />
);
}
case CALL_STATES.FINISHED: {
this.play("terminated");
- return this._renderFeedbackView();
+
+ // When conversation ended we either display a feedback form or
+ // close the window. This is decided in the AppControllerView.
+ return null;
}
case CALL_STATES.INIT: {
// We know what we are, but we haven't got the data yet.
return null;
}
default: {
return this._renderViewFromCallType();
}
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/feedbackViews.js
@@ -0,0 +1,47 @@
+var loop = loop || {};
+loop.feedbackViews = (function(_, mozL10n) {
+ "use strict";
+
+ /**
+ * Feedback view is displayed once every 6 months (loop.feedback.periodSec)
+ * after a conversation has ended.
+ */
+ var FeedbackView = React.createClass({displayName: "FeedbackView",
+ propTypes: {
+ mozLoop: React.PropTypes.object.isRequired,
+ onAfterFeedbackReceived: React.PropTypes.func.isRequired
+ },
+
+ /**
+ * Pressing the button to leave feedback will open the form in a new page
+ * and close the conversation window.
+ */
+ onFeedbackButtonClick: function() {
+ var url = this.props.mozLoop.getLoopPref("feedback.formURL");
+ this.props.mozLoop.openURL(url);
+
+ this.props.onAfterFeedbackReceived();
+ },
+
+ render: function() {
+ return (
+ React.createElement("div", {className: "feedback-view-container"},
+ React.createElement("h2", {className: "feedback-heading"},
+ mozL10n.get("feedback_window_heading")
+ ),
+ React.createElement("div", {className: "feedback-hello-logo"}),
+ React.createElement("div", {className: "feedback-button-container"},
+ React.createElement("button", {onClick: this.onFeedbackButtonClick,
+ ref: "feedbackFormBtn"},
+ mozL10n.get("feedback_request_button")
+ )
+ )
+ )
+ );
+ }
+ });
+
+ return {
+ FeedbackView: FeedbackView
+ };
+})(_, navigator.mozL10n || document.mozL10n);
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/js/feedbackViews.jsx
@@ -0,0 +1,47 @@
+var loop = loop || {};
+loop.feedbackViews = (function(_, mozL10n) {
+ "use strict";
+
+ /**
+ * Feedback view is displayed once every 6 months (loop.feedback.periodSec)
+ * after a conversation has ended.
+ */
+ var FeedbackView = React.createClass({
+ propTypes: {
+ mozLoop: React.PropTypes.object.isRequired,
+ onAfterFeedbackReceived: React.PropTypes.func.isRequired
+ },
+
+ /**
+ * Pressing the button to leave feedback will open the form in a new page
+ * and close the conversation window.
+ */
+ onFeedbackButtonClick: function() {
+ var url = this.props.mozLoop.getLoopPref("feedback.formURL");
+ this.props.mozLoop.openURL(url);
+
+ this.props.onAfterFeedbackReceived();
+ },
+
+ render: function() {
+ return (
+ <div className="feedback-view-container">
+ <h2 className="feedback-heading">
+ {mozL10n.get("feedback_window_heading")}
+ </h2>
+ <div className="feedback-hello-logo" />
+ <div className="feedback-button-container">
+ <button onClick={this.onFeedbackButtonClick}
+ ref="feedbackFormBtn">
+ {mozL10n.get("feedback_request_button")}
+ </button>
+ </div>
+ </div>
+ );
+ }
+ });
+
+ return {
+ FeedbackView: FeedbackView
+ };
+})(_, navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/js/roomViews.js
+++ b/browser/components/loop/content/js/roomViews.js
@@ -553,16 +553,17 @@ loop.roomViews = (function(mozL10n) {
sharedMixins.WindowCloseMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
mozLoop: React.PropTypes.object.isRequired,
+ onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
getInitialState: function() {
return {
contextEnabled: this.props.mozLoop.getLoopPref("contextInConversations.enabled"),
showEditContext: false
@@ -688,16 +689,24 @@ loop.roomViews = (function(mozL10n) {
handleEditContextClick: function() {
this.setState({ showEditContext: !this.state.showEditContext });
},
handleEditContextClose: function() {
this.setState({ showEditContext: false });
},
+ componentDidUpdate: function(prevProps, prevState) {
+ // Handle timestamp and window closing only when the call has terminated.
+ if (prevState.roomState === ROOM_STATES.ENDED &&
+ this.state.roomState === ROOM_STATES.ENDED) {
+ this.props.onCallTerminated();
+ }
+ },
+
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
}
var localStreamClasses = React.addons.classSet({
local: true,
"local-stream": true,
@@ -721,20 +730,19 @@ loop.roomViews = (function(mozL10n) {
// FULL case should never happen on desktop.
return (
React.createElement(loop.conversationViews.GenericFailureView, {
cancelCall: this.closeWindow,
failureReason: this.state.failureReason})
);
}
case ROOM_STATES.ENDED: {
- return (
- React.createElement(sharedViews.FeedbackView, {
- onAfterFeedbackReceived: this.closeWindow})
- );
+ // When conversation ended we either display a feedback form or
+ // close the window. This is decided in the AppControllerView.
+ return null;
}
default: {
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement("div", {className: "video-layout-wrapper"},
React.createElement("div", {className: "conversation room-conversation"},
React.createElement("div", {className: "media nested"},
--- a/browser/components/loop/content/js/roomViews.jsx
+++ b/browser/components/loop/content/js/roomViews.jsx
@@ -553,16 +553,17 @@ loop.roomViews = (function(mozL10n) {
sharedMixins.WindowCloseMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
mozLoop: React.PropTypes.object.isRequired,
+ onCallTerminated: React.PropTypes.func.isRequired,
remotePosterUrl: React.PropTypes.string,
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore).isRequired
},
getInitialState: function() {
return {
contextEnabled: this.props.mozLoop.getLoopPref("contextInConversations.enabled"),
showEditContext: false
@@ -688,16 +689,24 @@ loop.roomViews = (function(mozL10n) {
handleEditContextClick: function() {
this.setState({ showEditContext: !this.state.showEditContext });
},
handleEditContextClose: function() {
this.setState({ showEditContext: false });
},
+ componentDidUpdate: function(prevProps, prevState) {
+ // Handle timestamp and window closing only when the call has terminated.
+ if (prevState.roomState === ROOM_STATES.ENDED &&
+ this.state.roomState === ROOM_STATES.ENDED) {
+ this.props.onCallTerminated();
+ }
+ },
+
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
}
var localStreamClasses = React.addons.classSet({
local: true,
"local-stream": true,
@@ -721,20 +730,19 @@ loop.roomViews = (function(mozL10n) {
// FULL case should never happen on desktop.
return (
<loop.conversationViews.GenericFailureView
cancelCall={this.closeWindow}
failureReason={this.state.failureReason} />
);
}
case ROOM_STATES.ENDED: {
- return (
- <sharedViews.FeedbackView
- onAfterFeedbackReceived={this.closeWindow} />
- );
+ // When conversation ended we either display a feedback form or
+ // close the window. This is decided in the AppControllerView.
+ return null;
}
default: {
return (
<div className="room-conversation-wrapper">
<div className="video-layout-wrapper">
<div className="conversation room-conversation">
<div className="media nested">
--- a/browser/components/loop/content/shared/css/conversation.css
+++ b/browser/components/loop/content/shared/css/conversation.css
@@ -418,66 +418,57 @@
margin: 2em 0;
}
.promote-firefox h3 {
font-weight: 300;
}
/* Feedback form */
-
-.feedback {
- padding: 14px;
-}
-
-.feedback p {
- margin: 0px;
+.feedback-view-container {
+ display: flex;
+ flex-direction: column;
+ flex-wrap: nowrap;
+ justify-content: center;
+ align-content: center;
+ align-items: flex-start;
+ height: 100%;
}
-.feedback h3 {
- color: #666;
- font-size: 12px;
- font-weight: 700;
+.feedback-heading {
+ margin: 1em 0;
+ width: 100%;
text-align: center;
- margin: 0 0 1em 0;
-}
-
-.feedback .faces {
- display: flex;
- flex-direction: row;
- align-items: center;
- justify-content: center;
- padding: 20px 0;
+ font-weight: bold;
+ font-size: 1.2em;
}
-.feedback .face {
- border: 1px solid transparent;
- box-shadow: 0 1px 2px #CCC;
- cursor: pointer;
- border-radius: 4px;
- margin: 0 10px;
- width: 80px;
- height: 80px;
- background-color: #fbfbfb;
- background-size: 60px auto;
- background-position: center center;
+.feedback-hello-logo {
+ background-image: url("../img/helloicon.svg");
+ background-position: center;
+ background-size: contain;
background-repeat: no-repeat;
+ flex: 2 1 auto;
+ width: 100%;
+ margin: 30px 0;
}
-.feedback .face:hover {
- border: 1px solid #DDD;
- background-color: #FEFEFE;
+.feedback-button-container {
+ flex: 0 1 auto;
+ margin: 30px;
+ align-self: center;
}
-.feedback .face.face-happy {
- background-image: url("../img/happy.png");
-}
-
-.feedback .face.face-sad {
- background-image: url("../img/sad.png");
+.feedback-button-container button {
+ margin: 0 30px;
+ padding: .5em 2em;
+ border: none;
+ background: #4E92DF;
+ color: #fff;
+ cursor: pointer;
}
.fx-embedded-btn-back {
margin-bottom: 1rem;
padding: .2rem .8rem;
border: 1px solid #aaa;
border-radius: 2px;
background: transparent;
@@ -1542,34 +1533,29 @@ html[dir="rtl"] .text-chat-entry.receive
}
/* Text chat entry timestamp */
.text-chat-entry-timestamp {
margin: 0 .5em;
color: #aaa;
font-style: italic;
font-size: .8em;
- order: 0;
flex: 0 1 auto;
align-self: center;
}
/* Sent text chat entries should be on the right */
.text-chat-entry.sent {
justify-content: flex-end;
}
.received > .text-chat-entry-timestamp {
order: 2;
}
-.sent > .text-chat-entry-timestamp {
- order: 0;
-}
-
/* Pseudo element used to cover part between chat bubble and chat arrow. */
.text-chat-entry > p:after {
position: absolute;
background: #fff;
content: "";
}
.text-chat-entry.sent > p:after {
@@ -1626,17 +1612,16 @@ html[dir="rtl"] .text-chat-entry.receive
align-self: flex-end;
}
.text-chat-entry.received .text-chat-arrow {
margin-left: 0;
margin-right: -9px;
height: 10px;
background-image: url("../img/chatbubble-arrow-left.svg");
- order: 0;
align-self: auto;
}
html[dir="rtl"] .text-chat-arrow {
transform: scaleX(-1);
}
html[dir="rtl"] .text-chat-entry.sent .text-chat-arrow {
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/content/shared/img/helloicon.svg
@@ -0,0 +1,1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path fill-rule="evenodd" clip-rule="evenodd" fill="#4E92DF" d="M32 0C14.3 0 0 12.6 0 28.1c0 7.7 3.6 14.7 9.3 19.8-1 3.5-3 8.3-6.9 12.9.7 1.2 11.7-3 19.4-6.1 3.2.9 6.6 1.5 10.2 1.5 17.7 0 32-12.6 32-28.1S49.7 0 32 0zm9.6 16.9c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zm-19.3 0c2.3 0 4.2 1.9 4.2 4.2 0 2.3-1.9 4.2-4.2 4.2-2.3 0-4.2-1.9-4.2-4.2-.1-2.3 1.8-4.2 4.2-4.2zM32 47.7h-.1-.1c-8.6 0-18.1-5.5-20.3-14.9 5.8 2.7 13.8 3.8 20.4 3.8 6.6 0 14.7-1.2 20.4-3.8-2.2 9.3-11.7 14.9-20.3 14.9z"/></svg>
\ No newline at end of file
--- a/browser/components/loop/content/shared/js/actions.js
+++ b/browser/components/loop/content/shared/js/actions.js
@@ -516,62 +516,39 @@ loop.shared.actions = (function() {
JoinedRoom: Action.define("joinedRoom", {
apiKey: String,
sessionToken: String,
sessionId: String,
expires: Number
}),
/**
- * Used to indicate that the feedback cycle is completed and the countdown
- * finished.
+ * Used to indicate the user wishes to leave the room.
*/
- FeedbackComplete: Action.define("feedbackComplete", {
+ LeaveRoom: Action.define("leaveRoom", {
}),
/**
- * Used to indicate the user wishes to leave the room.
+ * Signals that the feedback view should be rendered.
*/
- LeaveRoom: Action.define("leaveRoom", {
+ ShowFeedbackForm: Action.define("showFeedbackForm", {
}),
/**
* Used to record a link click for metrics purposes.
*/
RecordClick: Action.define("recordClick", {
// Note: for ToS and Privacy links, this should be the link, for
// other links this should be a generic description so that we don't
// record what users are clicking, just the information about the fact
// they clicked the link in that spot (e.g. "Shared URL").
linkInfo: String
}),
/**
- * Requires detailed information on sad feedback.
- */
- RequireFeedbackDetails: Action.define("requireFeedbackDetails", {
- }),
-
- /**
- * Send feedback data.
- */
- SendFeedback: Action.define("sendFeedback", {
- happy: Boolean,
- category: String,
- description: String
- }),
-
- /**
- * Reacts on feedback submission error.
- */
- SendFeedbackError: Action.define("sendFeedbackError", {
- error: Error
- }),
-
- /**
* Used to inform of the current session, publisher and connection
* status.
*/
ConnectionStatus: Action.define("connectionStatus", {
event: String,
state: String,
connections: Number,
sendStreams: Number,
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackApiClient.js
+++ /dev/null
@@ -1,115 +0,0 @@
-/* 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/. */
-
-var loop = loop || {};
-loop.FeedbackAPIClient = (function($, _) {
- "use strict";
-
- /**
- * Feedback API client. Sends feedback data to an input.mozilla.com compatible
- * API.
- *
- * @param {String} baseUrl Base API url (required)
- * @param {Object} defaults Defaults field values for that client.
- *
- * Required defaults:
- * - {String} product Product name (required)
- *
- * Optional defaults:
- * - {String} platform Platform name, eg. "Windows 8", "Android", "Linux"
- * - {String} version Product version, eg. "22b2", "1.1"
- * - {String} channel Product channel, eg. "stable", "beta"
- * - {String} user_agent eg. Mozilla/5.0 (Mobile; rv:18.0) Gecko/18.0 Firefox/18.0
- *
- * @link http://fjord.readthedocs.org/en/latest/api.html
- */
- function FeedbackAPIClient(baseUrl, defaults) {
- this.baseUrl = baseUrl;
- if (!this.baseUrl) {
- throw new Error("Missing required 'baseUrl' argument.");
- }
-
- this.defaults = defaults || {};
- // required defaults checks
- if (!this.defaults.hasOwnProperty("product")) {
- throw new Error("Missing required 'product' default.");
- }
- }
-
- FeedbackAPIClient.prototype = {
- /**
- * Supported field names by the feedback API.
- * @type {Array}
- */
- _supportedFields: ["happy",
- "category",
- "description",
- "product",
- "platform",
- "version",
- "channel",
- "user_agent",
- "url"],
-
- /**
- * Creates a formatted payload object compliant with the Feedback API spec
- * against validated field data.
- *
- * @param {Object} fields Feedback initial values.
- * @return {Object} Formatted payload object.
- * @throws {Error} If provided values are invalid
- */
- _createPayload: function(fields) {
- if (typeof fields !== "object") {
- throw new Error("Invalid feedback data provided.");
- }
-
- Object.keys(fields).forEach(function(name) {
- if (this._supportedFields.indexOf(name) === -1) {
- throw new Error("Unsupported field " + name);
- }
- }, this);
-
- // Payload is basically defaults + fields merged in
- var payload = _.extend({}, this.defaults, fields);
-
- // Default description field value
- if (!fields.description) {
- payload.description = (fields.happy ? "Happy" : "Sad") + " User";
- }
-
- return payload;
- },
-
- /**
- * Sends feedback data.
- *
- * @param {Object} fields Feedback form data.
- * @param {Function} cb Callback(err, result)
- */
- send: function(fields, cb) {
- var req = $.ajax({
- url: this.baseUrl,
- method: "POST",
- contentType: "application/json",
- dataType: "json",
- data: JSON.stringify(this._createPayload(fields))
- });
-
- req.done(function(result) {
- console.info("User feedback data have been submitted", result);
- cb(null, result);
- });
-
- req.fail(function(jqXHR, textStatus, errorThrown) {
- var message = "Error posting user feedback data";
- var httpError = jqXHR.status + " " + errorThrown;
- cb(new Error(message + ": " + httpError + "; " +
- (jqXHR.responseJSON && jqXHR.responseJSON.detail || "")));
- });
- }
- };
-
- return FeedbackAPIClient;
-})(jQuery, _);
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackStore.js
+++ /dev/null
@@ -1,105 +0,0 @@
-/* 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/. */
-
-var loop = loop || {};
-loop.store = loop.store || {};
-
-loop.store.FeedbackStore = (function() {
- "use strict";
-
- var sharedActions = loop.shared.actions;
- var FEEDBACK_STATES = loop.store.FEEDBACK_STATES = {
- // Initial state (mood selection)
- INIT: "feedback-init",
- // User detailed feedback form step
- DETAILS: "feedback-details",
- // Pending feedback data submission
- PENDING: "feedback-pending",
- // Feedback has been sent
- SENT: "feedback-sent",
- // There was an issue with the feedback API
- FAILED: "feedback-failed"
- };
-
- /**
- * Feedback store.
- *
- * @param {loop.Dispatcher} dispatcher The dispatcher for dispatching actions
- * and registering to consume actions.
- * @param {Object} options Options object:
- * - {mozLoop} mozLoop The MozLoop API object.
- * - {feedbackClient} loop.FeedbackAPIClient The feedback API client.
- */
- var FeedbackStore = loop.store.createStore({
- actions: [
- "requireFeedbackDetails",
- "sendFeedback",
- "sendFeedbackError",
- "feedbackComplete"
- ],
-
- initialize: function(options) {
- if (!options.feedbackClient) {
- throw new Error("Missing option feedbackClient");
- }
- this._feedbackClient = options.feedbackClient;
- },
-
- /**
- * Returns initial state data for this active room.
- */
- getInitialStoreState: function() {
- return {feedbackState: FEEDBACK_STATES.INIT};
- },
-
- /**
- * Requires user detailed feedback.
- */
- requireFeedbackDetails: function() {
- this.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
- },
-
- /**
- * Sends feedback data to the feedback server.
- *
- * @param {sharedActions.SendFeedback} actionData The action data.
- */
- sendFeedback: function(actionData) {
- delete actionData.name;
- this._feedbackClient.send(actionData, function(err) {
- if (err) {
- this.dispatchAction(new sharedActions.SendFeedbackError({
- error: err
- }));
- return;
- }
- this.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
- }.bind(this));
-
- this.setStoreState({feedbackState: FEEDBACK_STATES.PENDING});
- },
-
- /**
- * Notifies a store from any error encountered while sending feedback data.
- *
- * @param {sharedActions.SendFeedback} actionData The action data.
- */
- sendFeedbackError: function(actionData) {
- this.setStoreState({
- feedbackState: FEEDBACK_STATES.FAILED,
- error: actionData.error
- });
- },
-
- /**
- * Resets the store to its initial state as feedback has been completed,
- * i.e. ready for the next round of feedback.
- */
- feedbackComplete: function() {
- this.resetStoreState();
- }
- });
-
- return FeedbackStore;
-})();
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackViews.js
+++ /dev/null
@@ -1,323 +0,0 @@
-/* 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/. */
-
-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.element,
- reset: React.PropTypes.func, // if not specified, no Back btn is shown
- title: React.PropTypes.string.isRequired
- },
-
- render: function() {
- var backButton = React.createElement("div", null);
- if (this.props.reset) {
- backButton = (
- React.createElement("button", {className: "fx-embedded-btn-back",
- onClick: this.props.reset,
- type: "button"},
- "« ", 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", {className: "feedback-category-label", key: key},
- React.createElement("input", {
- checked: this.state.category === category,
- className: "feedback-category-radio",
- name: "category",
- onChange: this.handleCategoryChange,
- ref: "category",
- type: "radio",
- value: 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, {
- reset: this.props.reset,
- title: l10n.get("feedback_category_list_heading")},
- React.createElement("form", {onSubmit: this.handleFormSubmit},
- this._getCategoryFields(),
- React.createElement("p", null,
- React.createElement("input", {className: "feedback-description",
- name: "description",
- onChange: this.handleDescriptionFieldChange,
- placeholder:
- l10n.get("feedback_custom_category_text_placeholder"),
- ref: "description",
- type: "text",
- value: this.state.description})
- ),
- React.createElement("button", {className: "btn btn-success",
- disabled: !this._isFormReady(),
- type: "submit"},
- 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: {
- noCloseText: React.PropTypes.bool,
- 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);
- }
- },
-
- _renderCloseText: function() {
- if (this.props.noCloseText) {
- return null;
- }
-
- return (
- React.createElement("p", {className: "info thank-you"},
- l10n.get("feedback_window_will_close_in2", {
- countdown: this.state.countdown,
- num: this.state.countdown
- }))
- );
- },
-
- render: function() {
- return (
- React.createElement(FeedbackLayout, {title: l10n.get("feedback_thank_you_heading")},
- this._renderCloseText()
- )
- );
- }
- });
-
- /**
- * Feedback view.
- */
- var FeedbackView = React.createClass({displayName: "FeedbackView",
- mixins: [
- Backbone.Events,
- loop.store.StoreMixin("feedbackStore")
- ],
-
- propTypes: {
- // Used by the UI showcase.
- feedbackState: React.PropTypes.string,
- noCloseText: React.PropTypes.bool,
- onAfterFeedbackReceived: React.PropTypes.func
- },
-
- 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(),
- pending: this.state.feedbackState === FEEDBACK_STATES.PENDING,
- reset: this.reset})
- );
- }
- 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, {
- noCloseText: this.props.noCloseText,
- onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
- );
- }
- }
- }
- });
-
- return FeedbackView;
-})(navigator.mozL10n || document.mozL10n);
deleted file mode 100644
--- a/browser/components/loop/content/shared/js/feedbackViews.jsx
+++ /dev/null
@@ -1,323 +0,0 @@
-/* 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/. */
-
-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({
- propTypes: {
- children: React.PropTypes.element,
- reset: React.PropTypes.func, // if not specified, no Back btn is shown
- title: React.PropTypes.string.isRequired
- },
-
- render: function() {
- var backButton = <div />;
- if (this.props.reset) {
- backButton = (
- <button className="fx-embedded-btn-back"
- onClick={this.props.reset}
- type="button" >
- « {l10n.get("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: {
- 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 (
- <label className="feedback-category-label" key={key}>
- <input
- checked={this.state.category === category}
- className="feedback-category-radio"
- name="category"
- onChange={this.handleCategoryChange}
- ref="category"
- type="radio"
- value={category} />
- {categories[category]}
- </label>
- );
- }, 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 (
- <FeedbackLayout
- reset={this.props.reset}
- title={l10n.get("feedback_category_list_heading")}>
- <form onSubmit={this.handleFormSubmit}>
- {this._getCategoryFields()}
- <p>
- <input className="feedback-description"
- name="description"
- onChange={this.handleDescriptionFieldChange}
- placeholder={
- l10n.get("feedback_custom_category_text_placeholder")}
- ref="description"
- type="text"
- value={this.state.description} />
- </p>
- <button className="btn btn-success"
- disabled={!this._isFormReady()}
- type="submit">
- {l10n.get("feedback_submit_button")}
- </button>
- </form>
- </FeedbackLayout>
- );
- }
- });
-
- /**
- * Feedback received view.
- *
- * Props:
- * - {Function} onAfterFeedbackReceived Function to execute after the
- * WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
- */
- var FeedbackReceived = React.createClass({
- propTypes: {
- noCloseText: React.PropTypes.bool,
- 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);
- }
- },
-
- _renderCloseText: function() {
- if (this.props.noCloseText) {
- return null;
- }
-
- return (
- <p className="info thank-you">{
- l10n.get("feedback_window_will_close_in2", {
- countdown: this.state.countdown,
- num: this.state.countdown
- })}</p>
- );
- },
-
- render: function() {
- return (
- <FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
- {this._renderCloseText()}
- </FeedbackLayout>
- );
- }
- });
-
- /**
- * Feedback view.
- */
- var FeedbackView = React.createClass({
- mixins: [
- Backbone.Events,
- loop.store.StoreMixin("feedbackStore")
- ],
-
- propTypes: {
- // Used by the UI showcase.
- feedbackState: React.PropTypes.string,
- noCloseText: React.PropTypes.bool,
- onAfterFeedbackReceived: React.PropTypes.func
- },
-
- 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 (
- <FeedbackLayout title={
- l10n.get("feedback_call_experience_heading2")}>
- <div className="faces">
- <button className="face face-happy"
- onClick={this.handleHappyClick}></button>
- <button className="face face-sad"
- onClick={this.handleSadClick}></button>
- </div>
- </FeedbackLayout>
- );
- }
- case FEEDBACK_STATES.DETAILS: {
- return (
- <FeedbackForm
- feedbackStore={this.getStore()}
- pending={this.state.feedbackState === FEEDBACK_STATES.PENDING}
- reset={this.reset} />
- );
- }
- 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 (
- <FeedbackReceived
- noCloseText={this.props.noCloseText}
- onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
- );
- }
- }
- }
- });
-
- return FeedbackView;
-})(navigator.mozL10n || document.mozL10n);
--- a/browser/components/loop/content/shared/js/mixins.js
+++ b/browser/components/loop/content/shared/js/mixins.js
@@ -17,17 +17,16 @@ loop.shared.mixins = (function() {
* Sets a new root object. This is useful for testing native DOM events so we
* can fake them. In beforeEach(), loop.shared.mixins.setRootObject is used to
* substitute a fake window, and in afterEach(), the real window object is
* replaced.
*
* @param {Object}
*/
function setRootObject(obj) {
- // console.log("loop.shared.mixins: rootObject set to " + obj);
rootObject = obj;
}
/**
* window.location mixin. Handles changes in the call url.
* Forces a reload of the page to ensure proper state of the webapp
*
* @type {Object}
--- a/browser/components/loop/jar.mn
+++ b/browser/components/loop/jar.mn
@@ -15,29 +15,29 @@ browser.jar:
content/browser/loop/js/conversation.js (content/js/conversation.js)
content/browser/loop/js/conversationAppStore.js (content/js/conversationAppStore.js)
content/browser/loop/js/otconfig.js (content/js/otconfig.js)
content/browser/loop/js/panel.js (content/js/panel.js)
content/browser/loop/js/contacts.js (content/js/contacts.js)
content/browser/loop/js/conversationViews.js (content/js/conversationViews.js)
content/browser/loop/js/roomStore.js (content/js/roomStore.js)
content/browser/loop/js/roomViews.js (content/js/roomViews.js)
+ content/browser/loop/js/feedbackViews.js (content/js/feedbackViews.js)
# Desktop styles
content/browser/loop/css/contacts.css (content/css/contacts.css)
content/browser/loop/css/panel.css (content/css/panel.css)
# Shared styles
content/browser/loop/shared/css/reset.css (content/shared/css/reset.css)
content/browser/loop/shared/css/common.css (content/shared/css/common.css)
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/helloicon.svg (content/shared/img/helloicon.svg)
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/spinner.svg (content/shared/img/spinner.svg)
# XXX could get rid of the png spinner usages and replace them with the svg
# one?
content/browser/loop/shared/img/spinner.png (content/shared/img/spinner.png)
content/browser/loop/shared/img/spinner@2x.png (content/shared/img/spinner@2x.png)
content/browser/loop/shared/img/chatbubble-arrow-left.svg (content/shared/img/chatbubble-arrow-left.svg)
@@ -76,24 +76,21 @@ browser.jar:
# Shared scripts
content/browser/loop/shared/js/actions.js (content/shared/js/actions.js)
content/browser/loop/shared/js/conversationStore.js (content/shared/js/conversationStore.js)
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
content/browser/loop/shared/js/roomStates.js (content/shared/js/roomStates.js)
content/browser/loop/shared/js/fxOSActiveRoomStore.js (content/shared/js/fxOSActiveRoomStore.js)
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.js)
- content/browser/loop/shared/js/feedbackStore.js (content/shared/js/feedbackStore.js)
content/browser/loop/shared/js/dispatcher.js (content/shared/js/dispatcher.js)
- content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.js)
content/browser/loop/shared/js/models.js (content/shared/js/models.js)
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.js)
content/browser/loop/shared/js/otSdkDriver.js (content/shared/js/otSdkDriver.js)
content/browser/loop/shared/js/views.js (content/shared/js/views.js)
- content/browser/loop/shared/js/feedbackViews.js (content/shared/js/feedbackViews.js)
content/browser/loop/shared/js/textChatStore.js (content/shared/js/textChatStore.js)
content/browser/loop/shared/js/textChatView.js (content/shared/js/textChatView.js)
content/browser/loop/shared/js/utils.js (content/shared/js/utils.js)
content/browser/loop/shared/js/validate.js (content/shared/js/validate.js)
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
# Shared libs
#ifdef DEBUG
--- a/browser/components/loop/standalone/content/css/webapp.css
+++ b/browser/components/loop/standalone/content/css/webapp.css
@@ -332,38 +332,16 @@ p.standalone-btn-label {
.standalone .ended-conversation {
position: relative;
height: 100%;
background-color: #444;
text-align: left; /* as backup */
text-align: start;
}
-.standalone .ended-conversation .feedback {
- position: absolute;
- width: 50%;
- max-width: 400px;
- margin: 10px auto;
- top: 20px;
- left: 10%;
- right: 10%;
- background: #FFF;
- box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
- border-radius: 3px;
- z-index: 1002; /* ensures the form is always on top of the control bar */
-}
-.standalone .room-conversation-wrapper .ended-conversation .feedback {
- right: 35%;
-}
-
-html[dir="rtl"] .standalone .room-conversation-wrapper .ended-conversation .feedback {
- right: auto;
- left: 35%;
-}
-
.standalone .ended-conversation .local-stream {
/* Hide local media stream when feedback form is shown. */
display: none;
}
@media screen and (max-width:640px) {
.standalone .ended-conversation .feedback {
width: 92%;
--- a/browser/components/loop/standalone/content/index.html
+++ b/browser/components/loop/standalone/content/index.html
@@ -129,27 +129,25 @@
<script type="text/javascript" src="shared/libs/backbone-1.2.1.js"></script>
<!-- app scripts -->
<script type="text/javascript" src="config.js"></script>
<script type="text/javascript" src="shared/js/utils.js"></script>
<script type="text/javascript" src="shared/js/crypto.js"></script>
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
- <script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="shared/js/actions.js"></script>
<script type="text/javascript" src="shared/js/validate.js"></script>
<script type="text/javascript" src="shared/js/dispatcher.js"></script>
<script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="shared/js/otSdkDriver.js"></script>
<script type="text/javascript" src="shared/js/store.js"></script>
<script type="text/javascript" src="shared/js/roomStates.js"></script>
<script type="text/javascript" src="shared/js/fxOSActiveRoomStore.js"></script>
<script type="text/javascript" src="shared/js/activeRoomStore.js"></script>
- <script type="text/javascript" src="shared/js/feedbackStore.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackViews.js"></script>
<script type="text/javascript" src="shared/js/textChatStore.js"></script>
<script type="text/javascript" src="shared/js/textChatView.js"></script>
<script type="text/javascript" src="js/standaloneAppStore.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
<script type="text/javascript" src="js/fxOSMarketplace.js"></script>
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.js
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.js
@@ -85,23 +85,16 @@ loop.standaloneRoomViews = (function(moz
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
failureReason: React.PropTypes.string,
isFirefox: React.PropTypes.bool.isRequired,
joinRoom: React.PropTypes.func.isRequired,
roomState: React.PropTypes.string.isRequired,
roomUsed: React.PropTypes.bool.isRequired
},
- onFeedbackSent: function() {
- // We pass a tick to prevent React warnings regarding nested updates.
- setTimeout(function() {
- this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
- }.bind(this));
- },
-
_renderCallToActionLink: function() {
if (this.props.isFirefox) {
return (
React.createElement("a", {className: "btn btn-info", href: loop.config.learnMoreUrl},
mozL10n.get("rooms_room_full_call_to_action_label", {
clientShortname: mozL10n.get("clientShortname2")
})
)
@@ -113,18 +106,18 @@ loop.standaloneRoomViews = (function(moz
brandShortname: mozL10n.get("brandShortname")
})
)
);
},
render: function() {
switch(this.props.roomState) {
+ case ROOM_STATES.ENDED:
case ROOM_STATES.READY: {
- // XXX: In ENDED state, we should rather display the feedback form.
return (
React.createElement("div", {className: "room-inner-info-area"},
React.createElement("button", {className: "btn btn-join btn-info",
onClick: this.props.joinRoom},
mozL10n.get("rooms_room_join_label")
)
)
);
@@ -167,32 +160,16 @@ loop.standaloneRoomViews = (function(moz
React.createElement("div", {className: "room-inner-info-area"},
React.createElement("p", {className: "full-room-message"},
mozL10n.get("rooms_room_full_label")
),
React.createElement("p", null, this._renderCallToActionLink())
)
);
}
- case ROOM_STATES.ENDED: {
- if (this.props.roomUsed) {
- return (
- React.createElement("div", {className: "ended-conversation"},
- React.createElement(sharedViews.FeedbackView, {
- noCloseText: true,
- onAfterFeedbackReceived: this.onFeedbackSent})
- )
- );
- }
-
- // In case the room was not used (no one was here), we
- // bypass the feedback form.
- this.onFeedbackSent();
- return null;
- }
case ROOM_STATES.FAILED: {
return (
React.createElement(StandaloneRoomFailureView, {
dispatcher: this.props.dispatcher,
failureReason: this.props.failureReason})
);
}
case ROOM_STATES.INIT:
--- a/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
+++ b/browser/components/loop/standalone/content/js/standaloneRoomViews.jsx
@@ -85,23 +85,16 @@ loop.standaloneRoomViews = (function(moz
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
failureReason: React.PropTypes.string,
isFirefox: React.PropTypes.bool.isRequired,
joinRoom: React.PropTypes.func.isRequired,
roomState: React.PropTypes.string.isRequired,
roomUsed: React.PropTypes.bool.isRequired
},
- onFeedbackSent: function() {
- // We pass a tick to prevent React warnings regarding nested updates.
- setTimeout(function() {
- this.props.activeRoomStore.dispatchAction(new sharedActions.FeedbackComplete());
- }.bind(this));
- },
-
_renderCallToActionLink: function() {
if (this.props.isFirefox) {
return (
<a className="btn btn-info" href={loop.config.learnMoreUrl}>
{mozL10n.get("rooms_room_full_call_to_action_label", {
clientShortname: mozL10n.get("clientShortname2")
})}
</a>
@@ -113,18 +106,18 @@ loop.standaloneRoomViews = (function(moz
brandShortname: mozL10n.get("brandShortname")
})}
</a>
);
},
render: function() {
switch(this.props.roomState) {
+ case ROOM_STATES.ENDED:
case ROOM_STATES.READY: {
- // XXX: In ENDED state, we should rather display the feedback form.
return (
<div className="room-inner-info-area">
<button className="btn btn-join btn-info"
onClick={this.props.joinRoom}>
{mozL10n.get("rooms_room_join_label")}
</button>
</div>
);
@@ -167,32 +160,16 @@ loop.standaloneRoomViews = (function(moz
<div className="room-inner-info-area">
<p className="full-room-message">
{mozL10n.get("rooms_room_full_label")}
</p>
<p>{this._renderCallToActionLink()}</p>
</div>
);
}
- case ROOM_STATES.ENDED: {
- if (this.props.roomUsed) {
- return (
- <div className="ended-conversation">
- <sharedViews.FeedbackView
- noCloseText={true}
- onAfterFeedbackReceived={this.onFeedbackSent} />
- </div>
- );
- }
-
- // In case the room was not used (no one was here), we
- // bypass the feedback form.
- this.onFeedbackSent();
- return null;
- }
case ROOM_STATES.FAILED: {
return (
<StandaloneRoomFailureView
dispatcher={this.props.dispatcher}
failureReason={this.props.failureReason} />
);
}
case ROOM_STATES.INIT:
--- a/browser/components/loop/standalone/content/js/webapp.js
+++ b/browser/components/loop/standalone/content/js/webapp.js
@@ -586,18 +586,16 @@ loop.webapp = (function($, _, OT, mozL10
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_conversation_ended")});
return (
React.createElement("div", {className: "ended-conversation"},
- React.createElement(sharedViews.FeedbackView, {
- onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}),
React.createElement(sharedViews.ConversationView, {
audio: {enabled: false, visible: false},
dispatcher: this.props.dispatcher,
initiate: false,
model: this.props.conversation,
sdk: this.props.sdk,
video: {enabled: false, visible: false}})
)
@@ -679,17 +677,16 @@ loop.webapp = (function($, _, OT, mozL10
},
shouldComponentUpdate: function(nextProps, nextState) {
// Only rerender if current state has actually changed
return nextState.callStatus !== this.state.callStatus;
},
resetCallStatus: function() {
- this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
return function() {
this.setState({callStatus: "start"});
}.bind(this);
},
/**
* Renders the conversation views.
*/
@@ -1019,23 +1016,16 @@ loop.webapp = (function($, _, OT, mozL10
function init() {
var standaloneMozLoop = new loop.StandaloneMozLoop({
baseServerUrl: loop.config.serverUrl
});
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
- var feedbackApiClient = new loop.FeedbackAPIClient(
- loop.config.feedbackApiUrl, {
- product: loop.config.feedbackProductName,
- user_agent: navigator.userAgent,
- url: document.location.origin
- });
-
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl
});
var sdkDriver = new loop.OTSdkDriver({
// For the standalone, always request data channels. If they aren't
// implemented on the client, there won't be a similar message to us, and
@@ -1062,42 +1052,31 @@ loop.webapp = (function($, _, OT, mozL10
sdk: OT
});
activeRoomStore = activeRoomStore ||
new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
});
- var feedbackClient = new loop.FeedbackAPIClient(
- loop.config.feedbackApiUrl, {
- product: loop.config.feedbackProductName,
- user_agent: navigator.userAgent,
- url: document.location.origin
- });
-
// Stores
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
sdk: OT
});
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: feedbackClient
- });
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: activeRoomStore
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
- feedbackStore: feedbackStore,
// This isn't used in any views, but is saved here to ensure it
// is kept alive.
standaloneMetricsStore: standaloneMetricsStore,
textChatStore: textChatStore
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());
--- a/browser/components/loop/standalone/content/js/webapp.jsx
+++ b/browser/components/loop/standalone/content/js/webapp.jsx
@@ -586,18 +586,16 @@ loop.webapp = (function($, _, OT, mozL10
},
render: function() {
document.title = mozL10n.get("standalone_title_with_status",
{clientShortname: mozL10n.get("clientShortname2"),
currentStatus: mozL10n.get("status_conversation_ended")});
return (
<div className="ended-conversation">
- <sharedViews.FeedbackView
- onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
<sharedViews.ConversationView
audio={{enabled: false, visible: false}}
dispatcher={this.props.dispatcher}
initiate={false}
model={this.props.conversation}
sdk={this.props.sdk}
video={{enabled: false, visible: false}} />
</div>
@@ -679,17 +677,16 @@ loop.webapp = (function($, _, OT, mozL10
},
shouldComponentUpdate: function(nextProps, nextState) {
// Only rerender if current state has actually changed
return nextState.callStatus !== this.state.callStatus;
},
resetCallStatus: function() {
- this.props.dispatcher.dispatch(new sharedActions.FeedbackComplete());
return function() {
this.setState({callStatus: "start"});
}.bind(this);
},
/**
* Renders the conversation views.
*/
@@ -1019,23 +1016,16 @@ loop.webapp = (function($, _, OT, mozL10
function init() {
var standaloneMozLoop = new loop.StandaloneMozLoop({
baseServerUrl: loop.config.serverUrl
});
// Older non-flux based items.
var notifications = new sharedModels.NotificationCollection();
- var feedbackApiClient = new loop.FeedbackAPIClient(
- loop.config.feedbackApiUrl, {
- product: loop.config.feedbackProductName,
- user_agent: navigator.userAgent,
- url: document.location.origin
- });
-
// New flux items.
var dispatcher = new loop.Dispatcher();
var client = new loop.StandaloneClient({
baseServerUrl: loop.config.serverUrl
});
var sdkDriver = new loop.OTSdkDriver({
// For the standalone, always request data channels. If they aren't
// implemented on the client, there won't be a similar message to us, and
@@ -1062,42 +1052,31 @@ loop.webapp = (function($, _, OT, mozL10
sdk: OT
});
activeRoomStore = activeRoomStore ||
new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: standaloneMozLoop,
sdkDriver: sdkDriver
});
- var feedbackClient = new loop.FeedbackAPIClient(
- loop.config.feedbackApiUrl, {
- product: loop.config.feedbackProductName,
- user_agent: navigator.userAgent,
- url: document.location.origin
- });
-
// Stores
var standaloneAppStore = new loop.store.StandaloneAppStore({
conversation: conversation,
dispatcher: dispatcher,
sdk: OT
});
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: feedbackClient
- });
var standaloneMetricsStore = new loop.store.StandaloneMetricsStore(dispatcher, {
activeRoomStore: activeRoomStore
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: sdkDriver
});
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
- feedbackStore: feedbackStore,
// This isn't used in any views, but is saved here to ensure it
// is kept alive.
standaloneMetricsStore: standaloneMetricsStore,
textChatStore: textChatStore
});
window.addEventListener("unload", function() {
dispatcher.dispatch(new sharedActions.WindowUnload());
--- a/browser/components/loop/standalone/content/l10n/en-US/loop.properties
+++ b/browser/components/loop/standalone/content/l10n/en-US/loop.properties
@@ -68,37 +68,16 @@ vendor_alttext={{vendorShortname}} logo
## LOCALIZATION NOTE (call_url_creation_date_label): Example output: (from May 26, 2014)
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_getting_media_description={{clientShortname}} requires access to your camera and microphone.
call_progress_getting_media_title=Waiting for media…
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
fxos_app_needed=Please install the {{fxosAppName}} app from the Firefox Marketplace.
-feedback_call_experience_heading2=How was your conversation?
-feedback_thank_you_heading=Thank you for your feedback!
-feedback_category_list_heading=What made you sad?
-feedback_category_audio_quality=Audio quality
-feedback_category_video_quality=Video quality
-feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing2=Confusing controls
-feedback_category_other2=Other
-feedback_custom_category_text_placeholder=What went wrong?
-feedback_submit_button=Submit
-feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in2):
-## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
-## In this item, don't translate the part between {{..}}
-feedback_window_will_close_in2={[ plural(countdown) ]}
-feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
-feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
-feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
-
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
--- a/browser/components/loop/test/desktop-local/conversationAppStore_test.js
+++ b/browser/components/loop/test/desktop-local/conversationAppStore_test.js
@@ -27,58 +27,113 @@ describe("loop.store.ConversationAppStor
it("should throw an error if mozLoop is missing", function() {
expect(function() {
new loop.store.ConversationAppStore({dispatcher: dispatcher});
}).to.Throw(/mozLoop/);
});
});
describe("#getWindowData", function() {
- var fakeWindowData, fakeGetWindowData, fakeMozLoop, store;
+ var fakeWindowData, fakeGetWindowData, fakeMozLoop, store, getLoopPrefStub;
+ var setLoopPrefStub;
beforeEach(function() {
fakeWindowData = {
type: "incoming",
callId: "123456"
};
fakeGetWindowData = {
windowId: "42"
};
+ getLoopPrefStub = sandbox.stub();
+ setLoopPrefStub = sandbox.stub();
+
fakeMozLoop = {
getConversationWindowData: function(windowId) {
if (windowId === "42") {
return fakeWindowData;
}
return null;
- }
+ },
+ getLoopPref: getLoopPrefStub,
+ setLoopPref: setLoopPrefStub
};
store = new loop.store.ConversationAppStore({
dispatcher: dispatcher,
mozLoop: fakeMozLoop
});
});
+ afterEach(function() {
+ sandbox.restore();
+ });
+
it("should fetch the window type from the mozLoop API", function() {
dispatcher.dispatch(new sharedActions.GetWindowData(fakeGetWindowData));
expect(store.getStoreState()).eql({
windowType: "incoming"
});
});
+ it("should have the feedback period in initial state", function() {
+ getLoopPrefStub.returns(42);
+
+ // Expect ms.
+ expect(store.getInitialStoreState().feedbackPeriod).to.eql(42 * 1000);
+ });
+
+ it("should have the dateLastSeen in initial state", function() {
+ getLoopPrefStub.returns(42);
+
+ // Expect ms.
+ expect(store.getInitialStoreState().feedbackTimestamp).to.eql(42 * 1000);
+ });
+
+ it("should fetch the correct pref for feedback period", function() {
+ store.getInitialStoreState();
+
+ sinon.assert.calledWithExactly(getLoopPrefStub, "feedback.periodSec");
+ });
+
+ it("should fetch the correct pref for feedback period", function() {
+ store.getInitialStoreState();
+
+ sinon.assert.calledWithExactly(getLoopPrefStub,
+ "feedback.dateLastSeenSec");
+ });
+
+ it("should set showFeedbackForm to true when action is triggered", function() {
+ var showFeedbackFormStub = sandbox.stub(store, "showFeedbackForm");
+
+ dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+
+ sinon.assert.calledOnce(showFeedbackFormStub);
+ });
+
+ it("should set feedback timestamp on ShowFeedbackForm action", function() {
+ var clock = sandbox.useFakeTimers();
+ // Make sure we round down the value.
+ clock.tick(1001);
+ dispatcher.dispatch(new sharedActions.ShowFeedbackForm());
+
+ sinon.assert.calledOnce(setLoopPrefStub);
+ sinon.assert.calledWithExactly(setLoopPrefStub,
+ "feedback.dateLastSeenSec", 1);
+ });
+
it("should dispatch a SetupWindowData action with the data from the mozLoop API",
function() {
sandbox.stub(dispatcher, "dispatch");
store.getWindowData(new sharedActions.GetWindowData(fakeGetWindowData));
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.SetupWindowData(_.extend({
windowId: fakeGetWindowData.windowId
}, fakeWindowData)));
});
});
-
});
--- a/browser/components/loop/test/desktop-local/conversationViews_test.js
+++ b/browser/components/loop/test/desktop-local/conversationViews_test.js
@@ -5,27 +5,27 @@ describe("loop.conversationViews", funct
"use strict";
var expect = chai.expect;
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var sandbox, view, dispatcher, contact, fakeAudioXHR, conversationStore;
- var fakeMozLoop, fakeWindow;
+ var fakeMozLoop, fakeWindow, fakeClock;
var CALL_STATES = loop.store.CALL_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var REST_ERRNOS = loop.shared.utils.REST_ERRNOS;
var WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
beforeEach(function() {
sandbox = sinon.sandbox.create();
- sandbox.useFakeTimers();
+ fakeClock = sandbox.useFakeTimers();
sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
dispatcher = new loop.Dispatcher();
sandbox.stub(dispatcher, "dispatch");
@@ -53,16 +53,17 @@ describe("loop.conversationViews", funct
fakeMozLoop = navigator.mozLoop = {
SHARING_ROOM_URL: {
EMAIL_FROM_CALLFAILED: 2,
EMAIL_FROM_CONVERSATION: 3
},
// Dummy function, stubbed below.
getLoopPref: function() {},
+ setLoopPref: sandbox.stub(),
calls: {
clearCallInProgress: sinon.stub()
},
composeEmail: sinon.spy(),
get appVersionInfo() {
return {
version: "42",
channel: "test",
@@ -90,28 +91,24 @@ describe("loop.conversationViews", funct
navigator: { mozLoop: fakeMozLoop },
close: sinon.stub(),
document: {},
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: {}
- });
conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: fakeMozLoop,
sdkDriver: {}
});
loop.store.StoreMixin.register({
- conversationStore: conversationStore,
- feedbackStore: feedbackStore
+ conversationStore: conversationStore
});
});
afterEach(function() {
loop.shared.mixins.setRootObject(window);
view = undefined;
delete navigator.mozLoop;
sandbox.restore();
@@ -603,30 +600,33 @@ describe("loop.conversationViews", funct
var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
expect(muteBtn.classList.contains("muted")).eql(true);
});
});
describe("CallControllerView", function() {
- var feedbackStore;
+ var onCallTerminatedStub;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.CallControllerView, {
dispatcher: dispatcher,
- mozLoop: fakeMozLoop
+ mozLoop: fakeMozLoop,
+ onCallTerminated: onCallTerminatedStub
}));
}
beforeEach(function() {
- feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: {}
- });
+ onCallTerminatedStub = sandbox.stub();
+ });
+
+ afterEach(function() {
+ sandbox.restore();
});
it("should set the document title to the callerId", function() {
conversationStore.setStoreState({
contact: contact
});
mountTestComponent();
@@ -699,34 +699,16 @@ describe("loop.conversationViews", funct
conversationStore.setStoreState({callState: CALL_STATES.ONGOING});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.OngoingConversationView);
});
- it("should render the FeedbackView when the call state is 'finished'",
- function() {
- conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
- view = mountTestComponent();
-
- TestUtils.findRenderedComponentWithType(view,
- loop.shared.views.FeedbackView);
- });
-
- it("should set the document title to conversation_has_ended when displaying the feedback view", function() {
- conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
-
- mountTestComponent();
-
- expect(fakeWindow.document.title).eql("conversation_has_ended");
- });
-
it("should play the terminated sound when the call state is 'finished'",
function() {
var fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
removeAttribute: sinon.spy()
};
sandbox.stub(window, "Audio").returns(fakeAudio);
@@ -751,16 +733,31 @@ describe("loop.conversationViews", funct
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.PendingConversationView);
conversationStore.setStoreState({callState: CALL_STATES.TERMINATED});
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.CallFailedView);
});
+
+ it("should call onCallTerminated when the call is finished", function() {
+ conversationStore.setStoreState({
+ callState: CALL_STATES.ONGOING
+ });
+
+ view = mountTestComponent({
+ callState: CALL_STATES.FINISHED
+ });
+ // Force a state change so that it triggers componentDidUpdate.
+ view.setState({ callState: CALL_STATES.FINISHED });
+
+ sinon.assert.calledOnce(onCallTerminatedStub);
+ sinon.assert.calledWithExactly(onCallTerminatedStub);
+ });
});
describe("AcceptCallView", function() {
var callView;
function mountTestComponent(extraProps) {
var props = _.extend({dispatcher: dispatcher, mozLoop: fakeMozLoop}, extraProps);
return TestUtils.renderIntoDocument(
--- a/browser/components/loop/test/desktop-local/conversation_test.js
+++ b/browser/components/loop/test/desktop-local/conversation_test.js
@@ -1,33 +1,35 @@
/* 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/. */
describe("loop.conversation", function() {
"use strict";
var expect = chai.expect;
+ var FeedbackView = loop.feedbackViews.FeedbackView;
var TestUtils = React.addons.TestUtils;
- var sharedModels = loop.shared.models,
- fakeWindow,
- sandbox;
+ var sharedActions = loop.shared.actions;
+ var sharedModels = loop.shared.models;
+ var fakeWindow, sandbox, getLoopPrefStub, setLoopPrefStub, mozL10nGet;
beforeEach(function() {
sandbox = sinon.sandbox.create();
+ setLoopPrefStub = sandbox.stub();
navigator.mozLoop = {
doNotDisturb: true,
getStrings: function() {
return JSON.stringify({textContent: "fakeText"});
},
get locale() {
return "en-US";
},
- setLoopPref: sinon.stub(),
+ setLoopPref: setLoopPrefStub,
getLoopPref: function(prefName) {
if (prefName == "debug.sdk") {
return false;
}
return "http://fake";
},
LOOP_SESSION_TYPE: {
@@ -58,17 +60,17 @@ describe("loop.conversation", function()
document: {},
addEventListener: function() {},
removeEventListener: function() {}
};
loop.shared.mixins.setRootObject(fakeWindow);
// XXX These stubs should be hoisted in a common file
// Bug 1040968
- sandbox.stub(document.mozL10n, "get", function(x) {
+ mozL10nGet = sandbox.stub(document.mozL10n, "get", function(x) {
return x;
});
document.mozL10n.initialize(navigator.mozLoop);
});
afterEach(function() {
loop.shared.mixins.setRootObject(window);
delete navigator.mozLoop;
@@ -127,17 +129,17 @@ describe("loop.conversation", function()
new loop.shared.actions.GetWindowData({
windowId: "42"
}));
});
});
describe("AppControllerView", function() {
var conversationStore, client, ccView, dispatcher;
- var conversationAppStore, roomStore;
+ var conversationAppStore, roomStore, feedbackPeriodMs = 15770000000;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
React.createElement(loop.conversation.AppControllerView, {
roomStore: roomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop
}));
@@ -210,10 +212,102 @@ describe("loop.conversation", function()
it("should display the GenericFailureView for failures", function() {
conversationAppStore.setStoreState({windowType: "failed"});
ccView = mountTestComponent();
TestUtils.findRenderedComponentWithType(ccView,
loop.conversationViews.GenericFailureView);
});
+
+ it("should set the correct title when rendering feedback view", function() {
+ conversationAppStore.setStoreState({showFeedbackForm: true});
+
+ ccView = mountTestComponent();
+
+ sinon.assert.calledWithExactly(mozL10nGet, "conversation_has_ended");
+ });
+
+ it("should render FeedbackView if showFeedbackForm state is true",
+ function() {
+ conversationAppStore.setStoreState({showFeedbackForm: true});
+
+ ccView = mountTestComponent();
+
+ TestUtils.findRenderedComponentWithType(ccView, FeedbackView);
+ });
+
+ it("should dispatch a ShowFeedbackForm action if timestamp is 0",
+ function() {
+ conversationAppStore.setStoreState({feedbackTimestamp: 0});
+ sandbox.stub(dispatcher, "dispatch");
+
+ ccView = mountTestComponent();
+
+ ccView.handleCallTerminated();
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithExactly(dispatcher.dispatch,
+ new sharedActions.ShowFeedbackForm());
+ });
+
+ it("should set feedback timestamp if delta is > feedback period",
+ function() {
+ var feedbackTimestamp = new Date() - feedbackPeriodMs;
+ conversationAppStore.setStoreState({
+ feedbackTimestamp: feedbackTimestamp,
+ feedbackPeriod: feedbackPeriodMs
+ });
+
+ ccView = mountTestComponent();
+
+ ccView.handleCallTerminated();
+
+ sinon.assert.calledOnce(setLoopPrefStub);
+ });
+
+ it("should dispatch a ShowFeedbackForm action if delta > feedback period",
+ function() {
+ var feedbackTimestamp = new Date() - feedbackPeriodMs;
+ conversationAppStore.setStoreState({
+ feedbackTimestamp: feedbackTimestamp,
+ feedbackPeriod: feedbackPeriodMs
+ });
+ sandbox.stub(dispatcher, "dispatch");
+
+ ccView = mountTestComponent();
+
+ ccView.handleCallTerminated();
+
+ sinon.assert.calledOnce(dispatcher.dispatch);
+ sinon.assert.calledWithExactly(dispatcher.dispatch,
+ new sharedActions.ShowFeedbackForm());
+ });
+
+ it("should close the window if delta < feedback period", function() {
+ var feedbackTimestamp = new Date().getTime();
+ conversationAppStore.setStoreState({
+ feedbackTimestamp: feedbackTimestamp,
+ feedbackPeriod: feedbackPeriodMs
+ });
+
+ ccView = mountTestComponent();
+ var closeWindowStub = sandbox.stub(ccView, "closeWindow");
+ ccView.handleCallTerminated();
+
+ sinon.assert.calledOnce(closeWindowStub);
+ });
+
+ it("should set the correct timestamp for dateLastSeenSec", function() {
+ var feedbackTimestamp = new Date().getTime();
+ conversationAppStore.setStoreState({
+ feedbackTimestamp: feedbackTimestamp,
+ feedbackPeriod: feedbackPeriodMs
+ });
+
+ ccView = mountTestComponent();
+ var closeWindowStub = sandbox.stub(ccView, "closeWindow");
+ ccView.handleCallTerminated();
+
+ sinon.assert.calledOnce(closeWindowStub);
+ });
});
});
new file mode 100644
--- /dev/null
+++ b/browser/components/loop/test/desktop-local/feedbackViews_test.js
@@ -0,0 +1,100 @@
+/* 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/. */
+
+describe("loop.feedbackViews", function() {
+ "use strict";
+
+ var FeedbackView = loop.feedbackViews.FeedbackView;
+ var l10n = navigator.mozL10n || document.mozL10n;
+ var TestUtils = React.addons.TestUtils;
+ var sandbox, mozL10nGet;
+
+ beforeEach(function() {
+ sandbox = sinon.sandbox.create();
+ mozL10nGet = sandbox.stub(l10n, "get", function(x) {
+ return "translated:" + x;
+ });
+ });
+
+ afterEach(function() {
+ sandbox.restore();
+ });
+
+ describe("FeedbackView", function() {
+ var openURLStub, getLoopPrefStub, feedbackReceivedStub;
+ var fakeURL = "fake.form", mozLoop, view;
+
+ function mountTestComponent(props) {
+ props = _.extend({
+ mozLoop: mozLoop,
+ onAfterFeedbackReceived: feedbackReceivedStub
+ }, props);
+
+ return TestUtils.renderIntoDocument(
+ React.createElement(FeedbackView, props));
+ }
+
+ beforeEach(function() {
+ openURLStub = sandbox.stub();
+ getLoopPrefStub = sandbox.stub();
+ feedbackReceivedStub = sandbox.stub();
+ mozLoop = {
+ openURL: openURLStub,
+ getLoopPref: getLoopPrefStub
+ };
+ });
+
+ afterEach(function() {
+ view = null;
+ });
+
+ it("should render a feedback view", function() {
+ view = mountTestComponent();
+
+ TestUtils.findRenderedComponentWithType(view, FeedbackView);
+ });
+
+ it("should render a button with correct text", function() {
+ view = mountTestComponent();
+
+ sinon.assert.calledWithExactly(mozL10nGet, "feedback_request_button");
+ });
+
+ it("should render a header with correct text", function() {
+ view = mountTestComponent();
+
+ sinon.assert.calledWithExactly(mozL10nGet, "feedback_window_heading");
+ });
+
+ it("should open a new page to the feedback form", function() {
+ mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
+ .returns(fakeURL);
+ view = mountTestComponent();
+
+ TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+ sinon.assert.calledOnce(openURLStub);
+ sinon.assert.calledWithExactly(openURLStub, fakeURL);
+ });
+
+ it("should fetch the feedback form URL from the prefs", function() {
+ mozLoop.getLoopPref = sinon.stub().withArgs("feedback.formURL")
+ .returns(fakeURL);
+ view = mountTestComponent();
+
+ TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+ sinon.assert.calledOnce(mozLoop.getLoopPref);
+ sinon.assert.calledWithExactly(mozLoop.getLoopPref, "feedback.formURL");
+ });
+
+ it("should close the window after opening the form", function() {
+ view = mountTestComponent();
+
+ TestUtils.Simulate.click(view.refs.feedbackFormBtn.getDOMNode());
+
+ sinon.assert.calledOnce(feedbackReceivedStub);
+ });
+ });
+});
--- a/browser/components/loop/test/desktop-local/index.html
+++ b/browser/components/loop/test/desktop-local/index.html
@@ -42,47 +42,46 @@
<script>
/*global chai,mocha */
chai.config.includeStack = true;
mocha.setup({ui: 'bdd', timeout: 10000});
</script>
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
- <script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
- <script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
- <script src="../../content/shared/js/feedbackViews.js"></script>
<script src="../../content/js/client.js"></script>
<script src="../../content/js/conversationAppStore.js"></script>
<script src="../../content/js/roomStore.js"></script>
<script src="../../content/js/roomViews.js"></script>
<script src="../../content/js/conversationViews.js"></script>
+ <script src="../../content/js/feedbackViews.js"></script>
<script src="../../content/js/conversation.js"></script>
<script type="text/javascript;version=1.8" src="../../content/js/contacts.js"></script>
<script src="../../content/js/panel.js"></script>
<!-- Test scripts -->
<script src="conversationAppStore_test.js"></script>
<script src="client_test.js"></script>
<script src="conversation_test.js"></script>
+ <script src="feedbackViews_test.js"></script>
<script src="panel_test.js"></script>
<script src="roomViews_test.js"></script>
<script src="conversationViews_test.js"></script>
<script src="contacts_test.js"></script>
<script src="l10n_test.js"></script>
<script src="roomStore_test.js"></script>
<script>
// Stop the default init functions running to avoid conflicts in tests
@@ -92,17 +91,17 @@
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {
chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
});
});
describe("Unexpected Warnings Check", function() {
it("should long only the warnings we expect", function() {
- chai.expect(caughtWarnings.length).to.eql(30);
+ chai.expect(caughtWarnings.length).to.eql(27);
});
});
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
--- a/browser/components/loop/test/desktop-local/roomViews_test.js
+++ b/browser/components/loop/test/desktop-local/roomViews_test.js
@@ -36,17 +36,18 @@ describe("loop.roomViews", function () {
roomName: "fakeName",
decryptedContext: {
roomName: "fakeName",
urls: []
}
}),
update: sinon.stub().callsArgWith(2, null)
},
- telemetryAddValue: sinon.stub()
+ telemetryAddValue: sinon.stub(),
+ setLoopPref: sandbox.stub()
};
fakeWindow = {
close: sinon.stub(),
document: {},
navigator: {
mozLoop: fakeMozLoop
},
@@ -196,18 +197,17 @@ describe("loop.roomViews", function () {
describe("Copy Button", function() {
beforeEach(function() {
view = mountTestComponent({
roomData: { roomUrl: "http://invalid" }
});
});
- it("should dispatch a CopyRoomUrl action when the copy button is " +
- "pressed", function() {
+ it("should dispatch a CopyRoomUrl action when the copy button is pressed", function() {
var copyBtn = view.getDOMNode().querySelector(".btn-copy");
React.addons.TestUtils.Simulate.click(copyBtn);
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch, new sharedActions.CopyRoomUrl({
roomUrl: "http://invalid",
from: "conversation"
@@ -285,40 +285,38 @@ describe("loop.roomViews", function () {
});
expect(view.getDOMNode().querySelector(".room-context")).to.not.eql(null);
});
});
});
describe("DesktopRoomConversationView", function() {
- var view;
+ var view, onCallTerminatedStub;
beforeEach(function() {
- loop.store.StoreMixin.register({
- feedbackStore: new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: {}
- })
- });
sandbox.stub(dispatcher, "dispatch");
fakeMozLoop.getLoopPref = function(prefName) {
if (prefName == "contextInConversations.enabled") {
return true;
}
return "test";
};
+ onCallTerminatedStub = sandbox.stub();
});
- function mountTestComponent() {
+ function mountTestComponent(props) {
+ props = _.extend({
+ dispatcher: dispatcher,
+ roomStore: roomStore,
+ mozLoop: fakeMozLoop,
+ onCallTerminated: onCallTerminatedStub
+ }, props);
return TestUtils.renderIntoDocument(
- React.createElement(loop.roomViews.DesktopRoomConversationView, {
- dispatcher: dispatcher,
- roomStore: roomStore,
- mozLoop: fakeMozLoop
- }));
+ React.createElement(loop.roomViews.DesktopRoomConversationView, props));
}
it("should dispatch a setMute action when the audio mute button is pressed",
function() {
view = mountTestComponent();
view.setState({audioMuted: true});
@@ -367,18 +365,17 @@ describe("loop.roomViews", function () {
view.setState({audioMuted: true});
var muteBtn = view.getDOMNode().querySelector(".btn-mute-audio");
expect(muteBtn.classList.contains("muted")).eql(true);
});
- it("should dispatch a `StartScreenShare` action when sharing is not active " +
- "and the screen share button is pressed", function() {
+ it("should dispatch a `StartScreenShare` action when sharing is not active and the screen share button is pressed", function() {
view = mountTestComponent();
view.setState({screenSharingState: SCREEN_SHARE_STATES.INACTIVE});
var muteBtn = view.getDOMNode().querySelector(".btn-mute-video");
React.addons.TestUtils.Simulate.click(muteBtn);
@@ -414,28 +411,26 @@ describe("loop.roomViews", function () {
describe("#componentWillUpdate", function() {
function expectActionDispatched(component) {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
}
- it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
- "is entered", function() {
+ it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state is entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.READY});
var component = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
expectActionDispatched(component);
});
- it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is " +
- "re-entered", function() {
+ it("should dispatch a `SetupStreamElements` action on MEDIA_WAIT state is re-entered", function() {
activeRoomStore.setStoreState({roomState: ROOM_STATES.ENDED});
var component = mountTestComponent();
activeRoomStore.setStoreState({roomState: ROOM_STATES.MEDIA_WAIT});
expectActionDispatched(component);
});
});
@@ -484,28 +479,28 @@ describe("loop.roomViews", function () {
activeRoomStore.setStoreState({roomState: ROOM_STATES.HAS_PARTICIPANTS});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view,
loop.roomViews.DesktopRoomConversationView);
});
- it("should render the FeedbackView if roomState is `ENDED`",
- function() {
- activeRoomStore.setStoreState({
- roomState: ROOM_STATES.ENDED,
- used: true
- });
+ it("should call onCallTerminated when the call ended", function() {
+ activeRoomStore.setStoreState({
+ roomState: ROOM_STATES.ENDED,
+ used: true
+ });
- view = mountTestComponent();
+ view = mountTestComponent();
+ // Force a state change so that it triggers componentDidUpdate
+ view.setState({ foo: "bar" });
- TestUtils.findRenderedComponentWithType(view,
- loop.shared.views.FeedbackView);
- });
+ sinon.assert.calledOnce(onCallTerminatedStub);
+ });
it("should display loading spinner when localSrcVideoObject is null",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.MEDIA_WAIT,
localSrcVideoObject: null
});
--- a/browser/components/loop/test/functional/test_1_browser_call.py
+++ b/browser/components/loop/test/functional/test_1_browser_call.py
@@ -147,26 +147,22 @@ class Test1BrowserCall(MarionetteTestCas
button = self.marionette.find_element(By.CLASS_NAME, "btn-screen-share")
button.click()
def standalone_check_remote_screenshare(self):
self.switch_to_standalone()
self.check_video(".screen-share-video")
- def remote_leave_room_and_verify_feedback(self):
+ def remote_leave_room(self):
self.switch_to_standalone()
button = self.marionette.find_element(By.CLASS_NAME, "btn-hangup")
button.click()
- # check that the feedback form is displayed
- feedback_form = self.wait_for_element_displayed(By.CLASS_NAME, "faces")
- self.assertEqual(feedback_form.tag_name, "div", "expect feedback form")
-
self.switch_to_chatbox()
# check that the local view reverts to the preview mode
self.wait_for_element_displayed(By.CLASS_NAME, "room-invitation-content")
def local_get_chatbox_window_expr(self, expr):
"""
:expr: a sub-expression which must begin with a property of the
global content window (e.g. "location.path")
@@ -255,15 +251,15 @@ class Test1BrowserCall(MarionetteTestCas
# self.local_enable_screenshare()
# self.standalone_check_remote_screenshare()
# We hangup on the remote (standalone) side, because this also leaves
# the local chatbox with the local publishing media still connected,
# which means that the local_check_connection_length below
# verifies that the connection is noted at the time the remote media
# drops, rather than waiting until the window closes.
- self.remote_leave_room_and_verify_feedback()
+ self.remote_leave_room()
self.local_check_connection_length_noted()
def tearDown(self):
self.loop_test_servers.shutdown()
MarionetteTestCase.tearDown(self)
--- a/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
+++ b/browser/components/loop/test/mochitest/browser_mozLoop_pluralStrings.js
@@ -10,14 +10,14 @@
Components.utils.import("resource://gre/modules/Promise.jsm", this);
add_task(loadLoopPanel);
add_task(function* test_mozLoop_pluralStrings() {
Assert.ok(gMozLoopAPI, "mozLoop should exist");
- var strings = JSON.parse(gMozLoopAPI.getStrings("feedback_window_will_close_in2"));
- Assert.equal(gMozLoopAPI.getPluralForm(0, strings.textContent),
- "This window will close in {{countdown}} seconds");
+ var strings = JSON.parse(gMozLoopAPI.getStrings("import_contacts_success_message"));
Assert.equal(gMozLoopAPI.getPluralForm(1, strings.textContent),
- "This window will close in {{countdown}} second");
+ "{{total}} contact was successfully imported.");
+ Assert.equal(gMozLoopAPI.getPluralForm(3, strings.textContent),
+ "{{total}} contacts were successfully imported.");
});
--- a/browser/components/loop/test/shared/activeRoomStore_test.js
+++ b/browser/components/loop/test/shared/activeRoomStore_test.js
@@ -561,40 +561,16 @@ describe("loop.store.ActiveRoomStore", f
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.UpdateRoomInfo(expectedData));
});
});
});
- describe("#feedbackComplete", function() {
- it("should set the room state to READY", function() {
- store.setStoreState({
- roomState: ROOM_STATES.ENDED,
- used: true
- });
-
- store.feedbackComplete(new sharedActions.FeedbackComplete());
-
- expect(store.getStoreState().roomState).eql(ROOM_STATES.READY);
- });
-
- it("should reset the 'used' state", function() {
- store.setStoreState({
- roomState: ROOM_STATES.ENDED,
- used: true
- });
-
- store.feedbackComplete(new sharedActions.FeedbackComplete());
-
- expect(store.getStoreState().used).eql(false);
- });
- });
-
describe("#videoDimensionsChanged", function() {
it("should not contain any video dimensions at the very start", function() {
expect(store.getStoreState()).eql(store.getInitialStoreState());
});
it("should update the store with new video dimensions", function() {
var actionData = {
isLocal: true,
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackApiClient_test.js
+++ /dev/null
@@ -1,184 +0,0 @@
-/* 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/. */
-
-describe("loop.FeedbackAPIClient", function() {
- "use strict";
-
- var expect = chai.expect;
- var sandbox,
- fakeXHR,
- requests = [];
-
- beforeEach(function() {
- sandbox = sinon.sandbox.create();
- fakeXHR = sandbox.useFakeXMLHttpRequest();
- requests = [];
- // https://github.com/cjohansen/Sinon.JS/issues/393
- fakeXHR.xhr.onCreate = function (xhr) {
- requests.push(xhr);
- };
- });
-
- afterEach(function() {
- sandbox.restore();
- });
-
- describe("#constructor", function() {
- it("should require a baseUrl setting", function() {
- expect(function() {
- return new loop.FeedbackAPIClient();
- }).to.Throw(/required 'baseUrl'/);
- });
-
- it("should require a product setting", function() {
- expect(function() {
- return new loop.FeedbackAPIClient("http://fake", {});
- }).to.Throw(/required 'product'/);
- });
- });
-
- describe("constructed", function() {
- var client;
-
- beforeEach(function() {
- client = new loop.FeedbackAPIClient("http://fake/feedback", {
- product: "Hello",
- version: "42b1"
- });
- });
-
- describe("#send", function() {
- it("should send happy feedback data", function() {
- var feedbackData = {
- happy: true,
- description: "Happy User"
- };
-
- client.send(feedbackData, function(){});
-
- expect(requests).to.have.length.of(1);
- expect(requests[0].url).to.be.equal("http://fake/feedback");
- expect(requests[0].method).to.be.equal("POST");
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.happy).eql(true);
- expect(parsed.description).eql("Happy User");
- });
-
- it("should send sad feedback data", function() {
- var feedbackData = {
- happy: false,
- category: "confusing"
- };
-
- client.send(feedbackData, function(){});
-
- expect(requests).to.have.length.of(1);
- expect(requests[0].url).to.be.equal("http://fake/feedback");
- expect(requests[0].method).to.be.equal("POST");
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.happy).eql(false);
- expect(parsed.product).eql("Hello");
- expect(parsed.category).eql("confusing");
- expect(parsed.description).eql("Sad User");
- });
-
- it("should send formatted feedback data", function() {
- client.send({
- happy: false,
- category: "other",
- description: "it's far too awesome!"
- }, function(){});
-
- expect(requests).to.have.length.of(1);
- expect(requests[0].url).eql("http://fake/feedback");
- expect(requests[0].method).eql("POST");
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.happy).eql(false);
- expect(parsed.product).eql("Hello");
- expect(parsed.category).eql("other");
- expect(parsed.description).eql("it's far too awesome!");
- });
-
- it("should send product information", function() {
- client.send({product: "Hello"}, function(){});
-
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.product).eql("Hello");
- });
-
- it("should send platform information when provided", function() {
- client.send({platform: "Windows 8"}, function(){});
-
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.platform).eql("Windows 8");
- });
-
- it("should send channel information when provided", function() {
- client.send({channel: "beta"}, function(){});
-
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.channel).eql("beta");
- });
-
- it("should send version information when provided", function() {
- client.send({version: "42b1"}, function(){});
-
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.version).eql("42b1");
- });
-
- it("should send user_agent information when provided", function() {
- client.send({user_agent: "MOZAGENT"}, function(){});
-
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.user_agent).eql("MOZAGENT");
- });
-
- it("should send url information when provided", function() {
- client.send({url: "http://fake.invalid"}, function(){});
-
- var parsed = JSON.parse(requests[0].requestBody);
- expect(parsed.url).eql("http://fake.invalid");
- });
-
- it("should throw on invalid feedback data", function() {
- expect(function() {
- client.send("invalid data", function(){});
- }).to.Throw(/Invalid/);
- });
-
- it("should throw on unsupported field name", function() {
- expect(function() {
- client.send({bleh: "bah"}, function(){});
- }).to.Throw(/Unsupported/);
- });
-
- it("should call passed callback on success", function() {
- var cb = sandbox.spy();
- var fakeResponseData = {description: "confusing"};
- client.send({category: "confusing"}, cb);
-
- requests[0].respond(200, {"Content-Type": "application/json"},
- JSON.stringify(fakeResponseData));
-
- sinon.assert.calledOnce(cb);
- sinon.assert.calledWithExactly(cb, null, fakeResponseData);
- });
-
- it("should call passed callback on error", function() {
- var cb = sandbox.spy();
- var fakeErrorData = {error: true};
- client.send({category: "confusing"}, cb);
-
- requests[0].respond(400, {"Content-Type": "application/json"},
- JSON.stringify(fakeErrorData));
-
- sinon.assert.calledOnce(cb);
- sinon.assert.calledWithExactly(cb, sinon.match(function(err) {
- return /Bad Request/.test(err);
- }));
- });
- });
- });
-});
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackStore_test.js
+++ /dev/null
@@ -1,121 +0,0 @@
-/* Any copyright is dedicated to the Public Domain.
- * http://creativecommons.org/publicdomain/zero/1.0/ */
-
-describe("loop.store.FeedbackStore", function () {
- "use strict";
-
- var expect = chai.expect;
- var sharedActions = loop.shared.actions;
- var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
- var sandbox, dispatcher, store, feedbackClient;
-
- beforeEach(function() {
- sandbox = sinon.sandbox.create();
-
- dispatcher = new loop.Dispatcher();
-
- feedbackClient = new loop.FeedbackAPIClient("http://invalid", {
- product: "Loop"
- });
-
- store = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: feedbackClient
- });
- });
-
- afterEach(function() {
- sandbox.restore();
- });
-
- describe("#constructor", function() {
- it("should throw an error if feedbackClient is missing", function() {
- expect(function() {
- new loop.store.FeedbackStore(dispatcher);
- }).to.Throw(/feedbackClient/);
- });
-
- it("should set the store to the INIT feedback state", function() {
- var fakeStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: feedbackClient
- });
-
- expect(fakeStore.getStoreState("feedbackState"))
- .eql(FEEDBACK_STATES.INIT);
- });
- });
-
- describe("#requireFeedbackDetails", function() {
- it("should transition to DETAILS state", function() {
- store.requireFeedbackDetails(new sharedActions.RequireFeedbackDetails());
-
- expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.DETAILS);
- });
- });
-
- describe("#sendFeedback", function() {
- var sadFeedbackData = {
- happy: false,
- category: "fakeCategory",
- description: "fakeDescription"
- };
-
- beforeEach(function() {
- store.requireFeedbackDetails();
- });
-
- it("should send feedback data over the feedback client", function() {
- sandbox.stub(feedbackClient, "send");
-
- store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-
- sinon.assert.calledOnce(feedbackClient.send);
- sinon.assert.calledWithMatch(feedbackClient.send, sadFeedbackData);
- });
-
- it("should transition to PENDING state", function() {
- sandbox.stub(feedbackClient, "send");
-
- store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
-
- expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.PENDING);
- });
-
- it("should transition to SENT state on successful submission", function(done) {
- sandbox.stub(feedbackClient, "send", function(data, cb) {
- cb(null);
- });
-
- store.once("change:feedbackState", function() {
- expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.SENT);
- done();
- });
-
- store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
- });
-
- it("should transition to FAILED state on failed submission", function(done) {
- sandbox.stub(feedbackClient, "send", function(data, cb) {
- cb(new Error("failed"));
- });
-
- store.once("change:feedbackState", function() {
- expect(store.getStoreState("feedbackState")).eql(FEEDBACK_STATES.FAILED);
- done();
- });
-
- store.sendFeedback(new sharedActions.SendFeedback(sadFeedbackData));
- });
- });
-
- describe("feedbackComplete", function() {
- it("should reset the store state", function() {
- store.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
- store.feedbackComplete();
-
- expect(store.getStoreState()).eql({
- feedbackState: FEEDBACK_STATES.INIT
- });
- });
- });
-});
deleted file mode 100644
--- a/browser/components/loop/test/shared/feedbackViews_test.js
+++ /dev/null
@@ -1,191 +0,0 @@
-/* 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/. */
-
-describe("loop.shared.views.FeedbackView", function() {
- "use strict";
-
- var expect = chai.expect;
- var l10n = navigator.mozL10n || document.mozL10n;
- var TestUtils = React.addons.TestUtils;
- var sharedActions = loop.shared.actions;
- var sharedViews = loop.shared.views;
-
- var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
- var sandbox, comp, dispatcher, fakeFeedbackClient, feedbackStore;
-
- beforeEach(function() {
- sandbox = sinon.sandbox.create();
- dispatcher = new loop.Dispatcher();
- fakeFeedbackClient = {send: sandbox.stub()};
- feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: fakeFeedbackClient
- });
- loop.store.StoreMixin.register({feedbackStore: feedbackStore});
- comp = TestUtils.renderIntoDocument(
- React.createElement(sharedViews.FeedbackView));
- });
-
- afterEach(function() {
- sandbox.restore();
- });
-
- // local test helpers
- function clickHappyFace(component) {
- var happyFace = component.getDOMNode().querySelector(".face-happy");
- TestUtils.Simulate.click(happyFace);
- }
-
- function clickSadFace(component) {
- var sadFace = component.getDOMNode().querySelector(".face-sad");
- TestUtils.Simulate.click(sadFace);
- }
-
- function fillSadFeedbackForm(component, category, text) {
- TestUtils.Simulate.change(
- component.getDOMNode().querySelector("[value='" + category + "']"));
-
- if (text) {
- TestUtils.Simulate.change(
- component.getDOMNode().querySelector("[name='description']"), {
- target: {value: "fake reason"}
- });
- }
- }
-
- function submitSadFeedbackForm(component, category, text) {
- TestUtils.Simulate.submit(component.getDOMNode().querySelector("form"));
- }
-
- describe("Happy feedback", function() {
- it("should dispatch a SendFeedback action", function() {
- var dispatch = sandbox.stub(dispatcher, "dispatch");
-
- clickHappyFace(comp);
-
- sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
- happy: true,
- category: "",
- description: ""
- }));
- });
-
- it("should thank the user once feedback data is sent", function() {
- feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
- expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
- expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
- .eql(null);
- });
-
- it("should not display the countdown text if noCloseText is true", function() {
- comp = TestUtils.renderIntoDocument(
- React.createElement(sharedViews.FeedbackView, {
- noCloseText: true
- }));
-
- expect(comp.getDOMNode().querySelector(".info.thank-you")).eql(null);
- });
- });
-
- describe("Sad feedback", function() {
- it("should bring the user to feedback form when clicking on the sad face",
- function() {
- clickSadFace(comp);
-
- expect(comp.getDOMNode().querySelectorAll("form")).not.eql(null);
- });
-
- it("should render a back button", function() {
- feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-
- expect(comp.getDOMNode().querySelector("button.fx-embedded-btn-back"))
- .not.eql(null);
- });
-
- it("should reset the view when clicking the back button", function() {
- feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
-
- TestUtils.Simulate.click(
- comp.getDOMNode().querySelector("button.fx-embedded-btn-back"));
-
- expect(comp.getDOMNode().querySelector(".faces")).not.eql(null);
- });
-
- it("should disable the form submit button when no category is chosen",
- function() {
- clickSadFace(comp);
-
- expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
- });
-
- it("should disable the form submit button when the 'other' category is " +
- "chosen but no description has been entered yet",
- function() {
- clickSadFace(comp);
- fillSadFeedbackForm(comp, "other");
-
- expect(comp.getDOMNode().querySelector("form button").disabled).eql(true);
- });
-
- it("should enable the form submit button when the 'other' category is " +
- "chosen and a description is entered",
- function() {
- clickSadFace(comp);
- fillSadFeedbackForm(comp, "other", "fake");
-
- expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
- });
-
- it("should enable the form submit button once a predefined category is " +
- "chosen",
- function() {
- clickSadFace(comp);
-
- fillSadFeedbackForm(comp, "confusing");
-
- expect(comp.getDOMNode().querySelector("form button").disabled).eql(false);
- });
-
- it("should send feedback data when the form is submitted", function() {
- var dispatch = sandbox.stub(dispatcher, "dispatch");
- feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.DETAILS});
- fillSadFeedbackForm(comp, "confusing");
-
- submitSadFeedbackForm(comp);
-
- sinon.assert.calledOnce(dispatch);
- sinon.assert.calledWithMatch(dispatch, new sharedActions.SendFeedback({
- happy: false,
- category: "confusing",
- description: ""
- }));
- });
-
- it("should send feedback data when user has entered a custom description",
- function() {
- clickSadFace(comp);
-
- fillSadFeedbackForm(comp, "other", "fake reason");
- submitSadFeedbackForm(comp);
-
- sinon.assert.calledOnce(fakeFeedbackClient.send);
- sinon.assert.calledWith(fakeFeedbackClient.send, {
- happy: false,
- category: "other",
- description: "fake reason"
- });
- });
-
- it("should thank the user when feedback data has been sent", function() {
- fakeFeedbackClient.send = function(data, cb) {
- cb();
- };
- clickSadFace(comp);
- fillSadFeedbackForm(comp, "confusing");
- submitSadFeedbackForm(comp);
-
- expect(comp.getDOMNode().querySelectorAll(".thank-you")).not.eql(null);
- });
- });
-});
--- a/browser/components/loop/test/shared/index.html
+++ b/browser/components/loop/test/shared/index.html
@@ -47,47 +47,41 @@
</script>
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/crypto.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
- <script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
<script src="../../content/shared/js/conversationStore.js"></script>
- <script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
- <script src="../../content/shared/js/feedbackViews.js"></script>
<!-- Test scripts -->
<script src="models_test.js"></script>
<script src="mixins_test.js"></script>
<script src="utils_test.js"></script>
<script src="crypto_test.js"></script>
<script src="views_test.js"></script>
<script src="websocket_test.js"></script>
- <script src="feedbackApiClient_test.js"></script>
- <script src="feedbackViews_test.js"></script>
<script src="validate_test.js"></script>
<script src="dispatcher_test.js"></script>
<script src="activeRoomStore_test.js"></script>
<script src="fxOSActiveRoomStore_test.js"></script>
<script src="conversationStore_test.js"></script>
- <script src="feedbackStore_test.js"></script>
<script src="otSdkDriver_test.js"></script>
<script src="store_test.js"></script>
<script src="textChatStore_test.js"></script>
<script src="textChatView_test.js"></script>
<script>
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {
chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
--- a/browser/components/loop/test/standalone/index.html
+++ b/browser/components/loop/test/standalone/index.html
@@ -44,29 +44,26 @@
chai.config.includeStack = true;
mocha.setup({ui: 'bdd', timeout: 10000});
</script>
<!-- App scripts -->
<script src="../../content/shared/js/utils.js"></script>
<script src="../../content/shared/js/models.js"></script>
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
- <script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../content/shared/js/actions.js"></script>
<script src="../../content/shared/js/validate.js"></script>
<script src="../../content/shared/js/dispatcher.js"></script>
<script src="../../content/shared/js/store.js"></script>
<script src="../../content/shared/js/roomStates.js"></script>
<script src="../../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../../content/shared/js/activeRoomStore.js"></script>
- <script src="../../content/shared/js/feedbackStore.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/textChatStore.js"></script>
<script src="../../content/shared/js/textChatView.js"></script>
- <script src="../../content/shared/js/feedbackViews.js"></script>
<script src="../../content/shared/js/otSdkDriver.js"></script>
<script src="../../standalone/content/js/multiplexGum.js"></script>
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/standaloneMozLoop.js"></script>
<script src="../../standalone/content/js/fxOSMarketplace.js"></script>
<script src="../../standalone/content/js/standaloneRoomViews.js"></script>
<script src="../../standalone/content/js/standaloneMetricsStore.js"></script>
@@ -83,17 +80,17 @@
describe("Uncaught Error Check", function() {
it("should load the tests without errors", function() {
chai.expect(uncaughtError && uncaughtError.message).to.be.undefined;
});
});
describe("Unexpected Warnings Check", function() {
it("should long only the warnings we expect", function() {
- chai.expect(caughtWarnings.length).to.eql(15);
+ chai.expect(caughtWarnings.length).to.eql(11);
});
});
mocha.run(function () {
$("#mocha").append("<p id='complete'>Complete.</p>");
});
</script>
</body>
--- a/browser/components/loop/test/standalone/standaloneRoomViews_test.js
+++ b/browser/components/loop/test/standalone/standaloneRoomViews_test.js
@@ -10,35 +10,31 @@ describe("loop.standaloneRoomViews", fun
var ROOM_STATES = loop.store.ROOM_STATES;
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var FAILURE_DETAILS = loop.shared.utils.FAILURE_DETAILS;
var ROOM_INFO_FAILURES = loop.shared.utils.ROOM_INFO_FAILURES;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
- var sandbox, dispatcher, activeRoomStore, feedbackStore, dispatch;
+ var sandbox, dispatcher, activeRoomStore, dispatch;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
dispatch = sandbox.stub(dispatcher, "dispatch");
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: {},
sdkDriver: {}
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {}
});
- feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: {}
- });
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
- feedbackStore: feedbackStore,
textChatStore: textChatStore
});
sandbox.useFakeTimers();
// Prevents audio request errors in the test console.
sandbox.useFakeXMLHttpRequest();
});
@@ -496,48 +492,16 @@ describe("loop.standaloneRoomViews", fun
TestUtils.Simulate.click(getLeaveButton(view));
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch, new sharedActions.LeaveRoom());
});
});
- describe("Feedback", function() {
- beforeEach(function() {
- activeRoomStore.setStoreState({
- roomState: ROOM_STATES.ENDED,
- used: true
- });
- });
-
- it("should display a feedback form when the user leaves the room",
- function() {
- expect(view.getDOMNode().querySelector(".faces")).not.eql(null);
- });
-
- it("should dispatch a `FeedbackComplete` action after feedback is sent",
- function() {
- feedbackStore.setStoreState({feedbackState: FEEDBACK_STATES.SENT});
-
- sandbox.clock.tick(
- loop.shared.views.WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS * 1000 + 1000);
-
- sinon.assert.calledOnce(dispatch);
- sinon.assert.calledWithExactly(dispatch, new sharedActions.FeedbackComplete());
- });
-
- it("should NOT display a feedback form if the room has not been used",
- function() {
- activeRoomStore.setStoreState({used: false});
- expect(view.getDOMNode().querySelector(".faces")).eql(null);
- });
-
- });
-
describe("Mute", function() {
it("should render a local avatar if video is muted",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.SESSION_CONNECTED,
videoMuted: true
});
--- a/browser/components/loop/test/standalone/webapp_test.js
+++ b/browser/components/loop/test/standalone/webapp_test.js
@@ -18,21 +18,16 @@ describe("loop.webapp", function() {
fakeAudioXHR,
dispatcher,
WEBSOCKET_REASONS = loop.shared.utils.WEBSOCKET_REASONS;
beforeEach(function() {
sandbox = sinon.sandbox.create();
dispatcher = new loop.Dispatcher();
notifications = new sharedModels.NotificationCollection();
- loop.store.StoreMixin.register({
- feedbackStore: new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: {}
- })
- });
stubGetPermsAndCacheMedia = sandbox.stub(
loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
fakeAudioXHR = {
open: sinon.spy(),
send: function() {},
abort: function() {},
@@ -49,17 +44,16 @@ describe("loop.webapp", function() {
afterEach(function() {
sandbox.restore();
});
describe("#init", function() {
beforeEach(function() {
sandbox.stub(React, "render");
- loop.config.feedbackApiUrl = "http://fake.invalid";
sandbox.stub(loop.Dispatcher.prototype, "dispatch");
});
it("should create the WebappRootView", function() {
loop.webapp.init();
sinon.assert.calledOnce(React.render);
sinon.assert.calledWith(React.render,
@@ -1072,20 +1066,16 @@ describe("loop.webapp", function() {
sdk: {},
onAfterFeedbackReceived: function(){}
}));
});
it("should render a ConversationView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
});
-
- it("should render a FeedbackView", function() {
- TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
- });
});
describe("PromoteFirefoxView", function() {
describe("#render", function() {
it("should not render when using Firefox", function() {
var comp = TestUtils.renderIntoDocument(
React.createElement(loop.webapp.PromoteFirefoxView, {
isFirefox: true
--- a/browser/components/loop/ui/index.html
+++ b/browser/components/loop/ui/index.html
@@ -37,33 +37,31 @@
window.OTProperties.configURL = window.OTProperties.assetURL + 'js/dynamic_config.min.js';
</script>
<script src="../content/js/multiplexGum.js"></script>
<script src="../content/shared/libs/sdk.js"></script>
<script src="../content/shared/libs/react-0.12.2.js"></script>
<script src="../content/shared/libs/jquery-2.1.4.js"></script>
<script src="../content/shared/libs/lodash-3.9.3.js"></script>
<script src="../content/shared/libs/backbone-1.2.1.js"></script>
- <script src="../content/shared/js/feedbackApiClient.js"></script>
<script src="../content/shared/js/actions.js"></script>
<script src="../content/shared/js/utils.js"></script>
<script src="../content/shared/js/models.js"></script>
<script src="../content/shared/js/mixins.js"></script>
<script src="../content/shared/js/websocket.js"></script>
<script src="../content/shared/js/validate.js"></script>
<script src="../content/shared/js/dispatcher.js"></script>
<script src="../content/shared/js/store.js"></script>
<script src="../content/shared/js/conversationStore.js"></script>
<script src="../content/shared/js/roomStates.js"></script>
<script src="../content/shared/js/fxOSActiveRoomStore.js"></script>
<script src="../content/shared/js/activeRoomStore.js"></script>
- <script src="../content/shared/js/feedbackStore.js"></script>
<script src="../content/shared/js/views.js"></script>
- <script src="../content/shared/js/feedbackViews.js"></script>
<script src="../content/shared/js/textChatStore.js"></script>
+ <script src="../content/js/feedbackViews.js"></script>
<script src="../content/shared/js/textChatView.js"></script>
<script src="../content/js/roomStore.js"></script>
<script src="../content/js/roomViews.js"></script>
<script src="../content/js/conversationViews.js"></script>
<script src="../content/js/client.js"></script>
<script src="../standalone/content/js/multiplexGum.js"></script>
<script src="../standalone/content/js/webapp.js"></script>
<script src="../standalone/content/js/standaloneRoomViews.js"></script>
--- a/browser/components/loop/ui/ui-showcase.js
+++ b/browser/components/loop/ui/ui-showcase.js
@@ -27,23 +27,22 @@
// 2. Standalone webapp
var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
- var FeedbackView = loop.shared.views.FeedbackView;
+ var FeedbackView = loop.feedbackViews.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
var TextChatView = loop.shared.views.chat.TextChatView;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
- var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
// Local helpers
function returnTrue() {
return true;
}
function returnFalse() {
@@ -71,24 +70,16 @@
}
window.removeEventListener(eventName, func);
};
loop.shared.mixins.setRootObject(rootObject);
var dispatcher = new loop.Dispatcher();
- // Feedback API client configured to send data to the stage input server,
- // which is available at https://input.allizom.org
- var stageFeedbackApiClient = new loop.FeedbackAPIClient(
- "https://input.allizom.org/api/v1/feedback", {
- product: "Loop"
- }
- );
-
var mockSDK = _.extend({
sendTextChatMessage: function(message) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
}));
}
}, Backbone.Events);
@@ -276,19 +267,16 @@
remoteVideoEnabled: false,
mediaConnected: true
});
var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: stageFeedbackApiClient
- });
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: mockSDK
});
@@ -349,17 +337,16 @@
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Cool",
sentTimestamp: "2015-06-23T22:27:45.590Z"
}));
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStore,
- feedbackStore: feedbackStore,
textChatStore: textChatStore
});
// Local mocks
var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
var mockContact = {
@@ -856,34 +843,23 @@
remoteVideoEnabled: false,
video: {enabled: true}})
)
)
),
React.createElement(Section, {name: "FeedbackView"},
- React.createElement("p", {className: "note"},
- React.createElement("strong", null, "Note:"), " For the useable demo, you can access submitted data at ",
- React.createElement("a", {href: "https://input.allizom.org/"}, "input.allizom.org"), "."
+ React.createElement("p", {className: "note"}
),
React.createElement(Example, {dashed: true,
style: {width: "300px", height: "272px"},
summary: "Default (useable demo)"},
- React.createElement(FeedbackView, {feedbackStore: feedbackStore})
- ),
- React.createElement(Example, {dashed: true,
- style: {width: "300px", height: "272px"},
- summary: "Detailed form"},
- React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.DETAILS, feedbackStore: feedbackStore})
- ),
- React.createElement(Example, {dashed: true,
- style: {width: "300px", height: "272px"},
- summary: "Thank you!"},
- React.createElement(FeedbackView, {feedbackState: FEEDBACK_STATES.SENT, feedbackStore: feedbackStore})
+ React.createElement(FeedbackView, {mozLoop: {},
+ onAfterFeedbackReceived: function() {}})
)
),
React.createElement(Section, {name: "AlertMessages"},
React.createElement(Example, {summary: "Various alerts"},
React.createElement("div", {className: "alert alert-warning"},
React.createElement("button", {className: "close"}),
React.createElement("p", {className: "message"},
@@ -921,16 +897,17 @@
height: 254,
summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
+ onCallTerminated: function(){},
roomState: ROOM_STATES.INIT,
roomStore: invitationRoomStore})
)
),
React.createElement(FramedExample, {
dashed: true,
height: 394,
@@ -938,56 +915,60 @@
width: 298},
/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */
React.createElement("div", {className: "fx-embedded overflow-hidden"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
+ onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
roomStore: desktopRoomStoreLoading})
)
),
React.createElement(FramedExample, {height: 254,
summary: "Desktop room conversation"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
+ onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS,
roomStore: roomStore})
)
),
React.createElement(FramedExample, {dashed: true,
height: 394,
summary: "Desktop room conversation local face-mute",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
+ onCallTerminated: function(){},
remotePosterUrl: "sample-img/video-screen-remote.png",
roomStore: desktopLocalFaceMuteRoomStore})
)
),
React.createElement(FramedExample, {dashed: true, height: 394,
summary: "Desktop room conversation remote face-mute",
width: 298},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
dispatcher: dispatcher,
localPosterUrl: "sample-img/video-screen-local.png",
mozLoop: navigator.mozLoop,
+ onCallTerminated: function(){},
roomStore: desktopRemoteFaceMuteRoomStore})
)
)
),
React.createElement(Section, {name: "StandaloneRoomView"},
React.createElement(FramedExample, {cssClass: "standalone",
dashed: true,
@@ -1169,30 +1150,16 @@
dispatcher: dispatcher,
isFirefox: false})
)
),
React.createElement(FramedExample, {cssClass: "standalone",
dashed: true,
height: 483,
- summary: "Standalone room conversation (feedback)",
- width: 644},
- React.createElement("div", {className: "standalone"},
- React.createElement(StandaloneRoomView, {
- activeRoomStore: endedRoomStore,
- dispatcher: dispatcher,
- feedbackStore: feedbackStore,
- isFirefox: false})
- )
- ),
-
- React.createElement(FramedExample, {cssClass: "standalone",
- dashed: true,
- height: 483,
summary: "Standalone room conversation (failed)",
width: 644},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
activeRoomStore: failedRoomStore,
dispatcher: dispatcher,
isFirefox: false})
)
@@ -1310,17 +1277,17 @@
setTimeout(waitForQueuedFrames, 500);
return;
}
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
- var expectedWarningsCount = 24;
+ var expectedWarningsCount = 23;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +
(!!(uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
if (warningsMismatch) {
$("#results").append("<li class='test fail'>" +
"<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
"<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/components/loop/ui/ui-showcase.jsx
+++ b/browser/components/loop/ui/ui-showcase.jsx
@@ -27,23 +27,22 @@
// 2. Standalone webapp
var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
- var FeedbackView = loop.shared.views.FeedbackView;
+ var FeedbackView = loop.feedbackViews.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
var TextChatView = loop.shared.views.chat.TextChatView;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
- var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
var CALL_TYPES = loop.shared.utils.CALL_TYPES;
// Local helpers
function returnTrue() {
return true;
}
function returnFalse() {
@@ -71,24 +70,16 @@
}
window.removeEventListener(eventName, func);
};
loop.shared.mixins.setRootObject(rootObject);
var dispatcher = new loop.Dispatcher();
- // Feedback API client configured to send data to the stage input server,
- // which is available at https://input.allizom.org
- var stageFeedbackApiClient = new loop.FeedbackAPIClient(
- "https://input.allizom.org/api/v1/feedback", {
- product: "Loop"
- }
- );
-
var mockSDK = _.extend({
sendTextChatMessage: function(message) {
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
message: message.message
}));
}
}, Backbone.Events);
@@ -276,19 +267,16 @@
remoteVideoEnabled: false,
mediaConnected: true
});
var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
- var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
- feedbackClient: stageFeedbackApiClient
- });
var conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: mockSDK
});
@@ -349,17 +337,16 @@
contentType: loop.store.CHAT_CONTENT_TYPES.TEXT,
message: "Cool",
sentTimestamp: "2015-06-23T22:27:45.590Z"
}));
loop.store.StoreMixin.register({
activeRoomStore: activeRoomStore,
conversationStore: conversationStore,
- feedbackStore: feedbackStore,
textChatStore: textChatStore
});
// Local mocks
var mockMozLoopRooms = _.extend({}, navigator.mozLoop);
var mockContact = {
@@ -857,33 +844,22 @@
video={{enabled: true}} />
</div>
</FramedExample>
</Section>
<Section name="FeedbackView">
<p className="note">
- <strong>Note:</strong> For the useable demo, you can access submitted data at
- <a href="https://input.allizom.org/">input.allizom.org</a>.
</p>
<Example dashed={true}
style={{width: "300px", height: "272px"}}
summary="Default (useable demo)">
- <FeedbackView feedbackStore={feedbackStore} />
- </Example>
- <Example dashed={true}
- style={{width: "300px", height: "272px"}}
- summary="Detailed form">
- <FeedbackView feedbackState={FEEDBACK_STATES.DETAILS} feedbackStore={feedbackStore} />
- </Example>
- <Example dashed={true}
- style={{width: "300px", height: "272px"}}
- summary="Thank you!">
- <FeedbackView feedbackState={FEEDBACK_STATES.SENT} feedbackStore={feedbackStore}/>
+ <FeedbackView mozLoop={{}}
+ onAfterFeedbackReceived={function() {}} />
</Example>
</Section>
<Section name="AlertMessages">
<Example summary="Various alerts">
<div className="alert alert-warning">
<button className="close"></button>
<p className="message">
@@ -921,16 +897,17 @@
height={254}
summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
width={298}>
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
+ onCallTerminated={function(){}}
roomState={ROOM_STATES.INIT}
roomStore={invitationRoomStore} />
</div>
</FramedExample>
<FramedExample
dashed={true}
height={394}
@@ -938,56 +915,60 @@
width={298}>
{/* Hide scrollbars here. Rotating loading div overflows and causes
scrollbars to appear */}
<div className="fx-embedded overflow-hidden">
<DesktopRoomConversationView
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
+ onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
roomStore={desktopRoomStoreLoading} />
</div>
</FramedExample>
<FramedExample height={254}
summary="Desktop room conversation">
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
+ onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS}
roomStore={roomStore} />
</div>
</FramedExample>
<FramedExample dashed={true}
height={394}
summary="Desktop room conversation local face-mute"
width={298}>
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
+ onCallTerminated={function(){}}
remotePosterUrl="sample-img/video-screen-remote.png"
roomStore={desktopLocalFaceMuteRoomStore} />
</div>
</FramedExample>
<FramedExample dashed={true} height={394}
summary="Desktop room conversation remote face-mute"
width={298} >
<div className="fx-embedded">
<DesktopRoomConversationView
dispatcher={dispatcher}
localPosterUrl="sample-img/video-screen-local.png"
mozLoop={navigator.mozLoop}
+ onCallTerminated={function(){}}
roomStore={desktopRemoteFaceMuteRoomStore} />
</div>
</FramedExample>
</Section>
<Section name="StandaloneRoomView">
<FramedExample cssClass="standalone"
dashed={true}
@@ -1169,30 +1150,16 @@
dispatcher={dispatcher}
isFirefox={false} />
</div>
</FramedExample>
<FramedExample cssClass="standalone"
dashed={true}
height={483}
- summary="Standalone room conversation (feedback)"
- width={644}>
- <div className="standalone">
- <StandaloneRoomView
- activeRoomStore={endedRoomStore}
- dispatcher={dispatcher}
- feedbackStore={feedbackStore}
- isFirefox={false} />
- </div>
- </FramedExample>
-
- <FramedExample cssClass="standalone"
- dashed={true}
- height={483}
summary="Standalone room conversation (failed)"
width={644} >
<div className="standalone">
<StandaloneRoomView
activeRoomStore={failedRoomStore}
dispatcher={dispatcher}
isFirefox={false} />
</div>
@@ -1310,17 +1277,17 @@
setTimeout(waitForQueuedFrames, 500);
return;
}
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
// This simulates the mocha layout for errors which means we can run
// this alongside our other unit tests but use the same harness.
- var expectedWarningsCount = 24;
+ var expectedWarningsCount = 23;
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
if (uncaughtError || warningsMismatch) {
$("#results").append("<div class='failures'><em>" +
(!!(uncaughtError && warningsMismatch) ? 2 : 1) + "</em></div>");
if (warningsMismatch) {
$("#results").append("<li class='test fail'>" +
"<h2>Unexpected number of warnings detected in UI-Showcase</h2>" +
"<pre class='error'>Got: " + caughtWarnings.length + "\n" +
--- a/browser/locales/en-US/chrome/browser/loop/loop.properties
+++ b/browser/locales/en-US/chrome/browser/loop/loop.properties
@@ -269,38 +269,23 @@ legal_text_tos = Terms of Use
legal_text_privacy = Privacy Notice
## LOCALIZATION NOTE (powered_by_beforeLogo, powered_by_afterLogo):
## These 2 strings are displayed before and after a 'Telefonica'
## logo.
powered_by_beforeLogo=Powered by
powered_by_afterLogo=
-feedback_call_experience_heading2=How was your conversation?
-feedback_thank_you_heading=Thank you for your feedback!
-feedback_category_list_heading=What made you sad?
-feedback_category_audio_quality=Audio quality
-feedback_category_video_quality=Video quality
-feedback_category_was_disconnected=Was disconnected
-feedback_category_confusing2=Confusing controls
-feedback_category_other2=Other
-feedback_custom_category_text_placeholder=What went wrong?
-feedback_submit_button=Submit
-feedback_back_button=Back
-## LOCALIZATION NOTE (feedback_window_will_close_in2):
-## Semicolon-separated list of plural forms. See:
-## http://developer.mozilla.org/en/docs/Localization_and_Plurals
-## In this item, don't translate the part between {{..}}
-feedback_window_will_close_in2=This window will close in {{countdown}} second;This window will close in {{countdown}} seconds
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User
+feedback_window_heading=How was your conversation?
feedback_request_button=Leave Feedback
help_label=Help
tour_label=Tour
## LOCALIZATION NOTE(rooms_default_room_name_template): {{conversationLabel}}
## will be replaced by a number. For example "Conversation 1" or "Conversation 12".
rooms_default_room_name_template=Conversation {{conversationLabel}}