зеркало из https://github.com/mozilla/gecko-dev.git
Merge fx-team to m-c a=merge
This commit is contained in:
Коммит
a98bb652c6
|
@ -1669,6 +1669,7 @@ pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds");
|
||||||
pref("loop.rooms.enabled", true);
|
pref("loop.rooms.enabled", true);
|
||||||
pref("loop.fxa_oauth.tokendata", "");
|
pref("loop.fxa_oauth.tokendata", "");
|
||||||
pref("loop.fxa_oauth.profile", "");
|
pref("loop.fxa_oauth.profile", "");
|
||||||
|
pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc");
|
||||||
|
|
||||||
// serverURL to be assigned by services team
|
// serverURL to be assigned by services team
|
||||||
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
|
pref("services.push.serverURL", "wss://push.services.mozilla.com/");
|
||||||
|
|
|
@ -12,6 +12,22 @@ function parseQueryString() {
|
||||||
|
|
||||||
document.title = parseQueryString();
|
document.title = parseQueryString();
|
||||||
|
|
||||||
|
addEventListener("DOMContentLoaded", () => {
|
||||||
|
let tryAgain = document.getElementById("tryAgain");
|
||||||
|
let sendCrashReport = document.getElementById("checkSendReport");
|
||||||
|
|
||||||
|
tryAgain.addEventListener("click", () => {
|
||||||
|
let event = new CustomEvent("AboutTabCrashedTryAgain", {
|
||||||
|
bubbles: true,
|
||||||
|
detail: {
|
||||||
|
sendCrashReport: sendCrashReport.checked,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
document.dispatchEvent(event);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
// Error pages are loaded as LOAD_BACKGROUND, so they don't get load events.
|
// Error pages are loaded as LOAD_BACKGROUND, so they don't get load events.
|
||||||
var event = new CustomEvent("AboutTabCrashedLoad", {bubbles:true});
|
var event = new CustomEvent("AboutTabCrashedLoad", {bubbles:true});
|
||||||
document.dispatchEvent(event);
|
document.dispatchEvent(event);
|
||||||
|
|
|
@ -1142,6 +1142,28 @@ var gBrowserInit = {
|
||||||
#endif
|
#endif
|
||||||
}, false, true);
|
}, false, true);
|
||||||
|
|
||||||
|
gBrowser.addEventListener("AboutTabCrashedTryAgain", function(event) {
|
||||||
|
let ownerDoc = event.originalTarget;
|
||||||
|
|
||||||
|
if (!ownerDoc.documentURI.startsWith("about:tabcrashed")) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let isTopFrame = (ownerDoc.defaultView.parent === ownerDoc.defaultView);
|
||||||
|
if (!isTopFrame) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let browser = gBrowser.getBrowserForDocument(ownerDoc);
|
||||||
|
#ifdef MOZ_CRASHREPORTER
|
||||||
|
if (event.detail.sendCrashReport) {
|
||||||
|
TabCrashReporter.submitCrashReport(browser);
|
||||||
|
}
|
||||||
|
#endif
|
||||||
|
let tab = gBrowser.getTabForBrowser(browser);
|
||||||
|
SessionStore.reviveCrashedTab(tab);
|
||||||
|
}, false, true);
|
||||||
|
|
||||||
if (uriToLoad && uriToLoad != "about:blank") {
|
if (uriToLoad && uriToLoad != "about:blank") {
|
||||||
if (uriToLoad instanceof Ci.nsISupportsArray) {
|
if (uriToLoad instanceof Ci.nsISupportsArray) {
|
||||||
let count = uriToLoad.Count();
|
let count = uriToLoad.Count();
|
||||||
|
@ -2606,9 +2628,6 @@ let BrowserOnClick = {
|
||||||
ownerDoc.documentURI.toLowerCase() == "about:newtab") {
|
ownerDoc.documentURI.toLowerCase() == "about:newtab") {
|
||||||
this.onE10sAboutNewTab(event, ownerDoc);
|
this.onE10sAboutNewTab(event, ownerDoc);
|
||||||
}
|
}
|
||||||
else if (ownerDoc.documentURI.startsWith("about:tabcrashed")) {
|
|
||||||
this.onAboutTabCrashed(event, ownerDoc);
|
|
||||||
}
|
|
||||||
},
|
},
|
||||||
|
|
||||||
receiveMessage: function (msg) {
|
receiveMessage: function (msg) {
|
||||||
|
@ -2869,29 +2888,6 @@ let BrowserOnClick = {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
|
||||||
* The about:tabcrashed can't do window.reload() because that
|
|
||||||
* would reload the page but not use a remote browser.
|
|
||||||
*/
|
|
||||||
onAboutTabCrashed: function(event, ownerDoc) {
|
|
||||||
let isTopFrame = (ownerDoc.defaultView.parent === ownerDoc.defaultView);
|
|
||||||
if (!isTopFrame) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
let button = event.originalTarget;
|
|
||||||
if (button.id == "tryAgain") {
|
|
||||||
let browser = gBrowser.getBrowserForDocument(ownerDoc);
|
|
||||||
#ifdef MOZ_CRASHREPORTER
|
|
||||||
if (ownerDoc.getElementById("checkSendReport").checked) {
|
|
||||||
TabCrashReporter.submitCrashReport(browser);
|
|
||||||
}
|
|
||||||
#endif
|
|
||||||
let tab = gBrowser.getTabForBrowser(browser);
|
|
||||||
SessionStore.reviveCrashedTab(tab);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
ignoreWarningButton: function (isMalware) {
|
ignoreWarningButton: function (isMalware) {
|
||||||
// Allow users to override and continue through to the site,
|
// Allow users to override and continue through to the site,
|
||||||
// but add a notify bar as a reminder, so that they don't lose
|
// but add a notify bar as a reminder, so that they don't lose
|
||||||
|
|
|
@ -38,6 +38,8 @@
|
||||||
<script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
|
<script type="text/javascript" src="loop/shared/js/roomStore.js"></script>
|
||||||
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
|
<script type="text/javascript" src="loop/shared/js/conversationStore.js"></script>
|
||||||
<script type="text/javascript" src="loop/shared/js/activeRoomStore.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/feedbackViews.js"></script>
|
||||||
<script type="text/javascript" src="loop/js/conversationViews.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/shared/js/websocket.js"></script>
|
||||||
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
|
<script type="text/javascript" src="loop/js/conversationAppStore.js"></script>
|
||||||
|
|
|
@ -229,7 +229,8 @@ loop.conversation = (function(mozL10n) {
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
conversationAppStore: React.PropTypes.instanceOf(
|
conversationAppStore: React.PropTypes.instanceOf(
|
||||||
loop.store.ConversationAppStore).isRequired
|
loop.store.ConversationAppStore).isRequired,
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -301,21 +302,9 @@ loop.conversation = (function(mozL10n) {
|
||||||
|
|
||||||
document.title = mozL10n.get("conversation_has_ended");
|
document.title = mozL10n.get("conversation_has_ended");
|
||||||
|
|
||||||
var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
|
|
||||||
"feedback.baseUrl");
|
|
||||||
|
|
||||||
var appVersionInfo = navigator.mozLoop.appVersionInfo;
|
|
||||||
|
|
||||||
var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
|
|
||||||
product: navigator.mozLoop.getLoopPref("feedback.product"),
|
|
||||||
platform: appVersionInfo.OS,
|
|
||||||
channel: appVersionInfo.channel,
|
|
||||||
version: appVersionInfo.version
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sharedViews.FeedbackView({
|
sharedViews.FeedbackView({
|
||||||
feedbackApiClient: feedbackClient,
|
feedbackStore: this.props.feedbackStore,
|
||||||
onAfterFeedbackReceived: this.closeWindow.bind(this)}
|
onAfterFeedbackReceived: this.closeWindow.bind(this)}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -562,7 +551,8 @@ loop.conversation = (function(mozL10n) {
|
||||||
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
|
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
|
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -590,26 +580,26 @@ loop.conversation = (function(mozL10n) {
|
||||||
client: this.props.client,
|
client: this.props.client,
|
||||||
conversation: this.props.conversation,
|
conversation: this.props.conversation,
|
||||||
sdk: this.props.sdk,
|
sdk: this.props.sdk,
|
||||||
conversationAppStore: this.props.conversationAppStore}
|
conversationAppStore: this.props.conversationAppStore,
|
||||||
|
feedbackStore: this.props.feedbackStore}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
case "outgoing": {
|
case "outgoing": {
|
||||||
return (OutgoingConversationView({
|
return (OutgoingConversationView({
|
||||||
store: this.props.conversationStore,
|
store: this.props.conversationStore,
|
||||||
dispatcher: this.props.dispatcher}
|
dispatcher: this.props.dispatcher,
|
||||||
|
feedbackStore: this.props.feedbackStore}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
case "room": {
|
case "room": {
|
||||||
return (DesktopRoomConversationView({
|
return (DesktopRoomConversationView({
|
||||||
dispatcher: this.props.dispatcher,
|
dispatcher: this.props.dispatcher,
|
||||||
roomStore: this.props.roomStore,
|
roomStore: this.props.roomStore,
|
||||||
dispatcher: this.props.dispatcher}
|
feedbackStore: this.props.feedbackStore}
|
||||||
));
|
));
|
||||||
}
|
}
|
||||||
case "failed": {
|
case "failed": {
|
||||||
return (GenericFailureView({
|
return GenericFailureView({cancelCall: this.closeWindow});
|
||||||
cancelCall: this.closeWindow}
|
|
||||||
));
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
// If we don't have a windowType, we don't know what we are yet,
|
// If we don't have a windowType, we don't know what we are yet,
|
||||||
|
@ -646,6 +636,14 @@ loop.conversation = (function(mozL10n) {
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
sdk: OT
|
sdk: OT
|
||||||
});
|
});
|
||||||
|
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.
|
// Create the stores.
|
||||||
var conversationAppStore = new loop.store.ConversationAppStore({
|
var conversationAppStore = new loop.store.ConversationAppStore({
|
||||||
|
@ -665,6 +663,9 @@ loop.conversation = (function(mozL10n) {
|
||||||
mozLoop: navigator.mozLoop,
|
mozLoop: navigator.mozLoop,
|
||||||
activeRoomStore: activeRoomStore
|
activeRoomStore: activeRoomStore
|
||||||
});
|
});
|
||||||
|
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: feedbackClient
|
||||||
|
});
|
||||||
|
|
||||||
// XXX Old class creation for the incoming conversation view, whilst
|
// XXX Old class creation for the incoming conversation view, whilst
|
||||||
// we transition across (bug 1072323).
|
// we transition across (bug 1072323).
|
||||||
|
@ -697,6 +698,7 @@ loop.conversation = (function(mozL10n) {
|
||||||
React.renderComponent(AppControllerView({
|
React.renderComponent(AppControllerView({
|
||||||
conversationAppStore: conversationAppStore,
|
conversationAppStore: conversationAppStore,
|
||||||
roomStore: roomStore,
|
roomStore: roomStore,
|
||||||
|
feedbackStore: feedbackStore,
|
||||||
conversationStore: conversationStore,
|
conversationStore: conversationStore,
|
||||||
client: client,
|
client: client,
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
|
|
|
@ -229,7 +229,8 @@ loop.conversation = (function(mozL10n) {
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
conversationAppStore: React.PropTypes.instanceOf(
|
conversationAppStore: React.PropTypes.instanceOf(
|
||||||
loop.store.ConversationAppStore).isRequired
|
loop.store.ConversationAppStore).isRequired,
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -301,21 +302,9 @@ loop.conversation = (function(mozL10n) {
|
||||||
|
|
||||||
document.title = mozL10n.get("conversation_has_ended");
|
document.title = mozL10n.get("conversation_has_ended");
|
||||||
|
|
||||||
var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
|
|
||||||
"feedback.baseUrl");
|
|
||||||
|
|
||||||
var appVersionInfo = navigator.mozLoop.appVersionInfo;
|
|
||||||
|
|
||||||
var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
|
|
||||||
product: navigator.mozLoop.getLoopPref("feedback.product"),
|
|
||||||
platform: appVersionInfo.OS,
|
|
||||||
channel: appVersionInfo.channel,
|
|
||||||
version: appVersionInfo.version
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<sharedViews.FeedbackView
|
<sharedViews.FeedbackView
|
||||||
feedbackApiClient={feedbackClient}
|
feedbackStore={this.props.feedbackStore}
|
||||||
onAfterFeedbackReceived={this.closeWindow.bind(this)}
|
onAfterFeedbackReceived={this.closeWindow.bind(this)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -562,7 +551,8 @@ loop.conversation = (function(mozL10n) {
|
||||||
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
|
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||||
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore)
|
roomStore: React.PropTypes.instanceOf(loop.store.RoomStore),
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -591,25 +581,25 @@ loop.conversation = (function(mozL10n) {
|
||||||
conversation={this.props.conversation}
|
conversation={this.props.conversation}
|
||||||
sdk={this.props.sdk}
|
sdk={this.props.sdk}
|
||||||
conversationAppStore={this.props.conversationAppStore}
|
conversationAppStore={this.props.conversationAppStore}
|
||||||
|
feedbackStore={this.props.feedbackStore}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
case "outgoing": {
|
case "outgoing": {
|
||||||
return (<OutgoingConversationView
|
return (<OutgoingConversationView
|
||||||
store={this.props.conversationStore}
|
store={this.props.conversationStore}
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
|
feedbackStore={this.props.feedbackStore}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
case "room": {
|
case "room": {
|
||||||
return (<DesktopRoomConversationView
|
return (<DesktopRoomConversationView
|
||||||
dispatcher={this.props.dispatcher}
|
dispatcher={this.props.dispatcher}
|
||||||
roomStore={this.props.roomStore}
|
roomStore={this.props.roomStore}
|
||||||
dispatcher={this.props.dispatcher}
|
feedbackStore={this.props.feedbackStore}
|
||||||
/>);
|
/>);
|
||||||
}
|
}
|
||||||
case "failed": {
|
case "failed": {
|
||||||
return (<GenericFailureView
|
return <GenericFailureView cancelCall={this.closeWindow} />;
|
||||||
cancelCall={this.closeWindow}
|
|
||||||
/>);
|
|
||||||
}
|
}
|
||||||
default: {
|
default: {
|
||||||
// If we don't have a windowType, we don't know what we are yet,
|
// If we don't have a windowType, we don't know what we are yet,
|
||||||
|
@ -646,6 +636,14 @@ loop.conversation = (function(mozL10n) {
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
sdk: OT
|
sdk: OT
|
||||||
});
|
});
|
||||||
|
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.
|
// Create the stores.
|
||||||
var conversationAppStore = new loop.store.ConversationAppStore({
|
var conversationAppStore = new loop.store.ConversationAppStore({
|
||||||
|
@ -665,6 +663,9 @@ loop.conversation = (function(mozL10n) {
|
||||||
mozLoop: navigator.mozLoop,
|
mozLoop: navigator.mozLoop,
|
||||||
activeRoomStore: activeRoomStore
|
activeRoomStore: activeRoomStore
|
||||||
});
|
});
|
||||||
|
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: feedbackClient
|
||||||
|
});
|
||||||
|
|
||||||
// XXX Old class creation for the incoming conversation view, whilst
|
// XXX Old class creation for the incoming conversation view, whilst
|
||||||
// we transition across (bug 1072323).
|
// we transition across (bug 1072323).
|
||||||
|
@ -697,6 +698,7 @@ loop.conversation = (function(mozL10n) {
|
||||||
React.renderComponent(<AppControllerView
|
React.renderComponent(<AppControllerView
|
||||||
conversationAppStore={conversationAppStore}
|
conversationAppStore={conversationAppStore}
|
||||||
roomStore={roomStore}
|
roomStore={roomStore}
|
||||||
|
feedbackStore={feedbackStore}
|
||||||
conversationStore={conversationStore}
|
conversationStore={conversationStore}
|
||||||
client={client}
|
client={client}
|
||||||
conversation={conversation}
|
conversation={conversation}
|
||||||
|
|
|
@ -356,7 +356,7 @@ loop.conversationViews = (function(mozL10n) {
|
||||||
nameDisplayMode: "off",
|
nameDisplayMode: "off",
|
||||||
videoDisabledDisplayMode: "off"
|
videoDisabledDisplayMode: "off"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -431,7 +431,8 @@ loop.conversationViews = (function(mozL10n) {
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||||
store: React.PropTypes.instanceOf(
|
store: React.PropTypes.instanceOf(
|
||||||
loop.store.ConversationStore).isRequired
|
loop.store.ConversationStore).isRequired,
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -462,22 +463,9 @@ loop.conversationViews = (function(mozL10n) {
|
||||||
_renderFeedbackView: function() {
|
_renderFeedbackView: function() {
|
||||||
document.title = mozL10n.get("conversation_has_ended");
|
document.title = mozL10n.get("conversation_has_ended");
|
||||||
|
|
||||||
// XXX Bug 1076754 Feedback view should be redone in the Flux style.
|
|
||||||
var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
|
|
||||||
"feedback.baseUrl");
|
|
||||||
|
|
||||||
var appVersionInfo = navigator.mozLoop.appVersionInfo;
|
|
||||||
|
|
||||||
var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
|
|
||||||
product: navigator.mozLoop.getLoopPref("feedback.product"),
|
|
||||||
platform: appVersionInfo.OS,
|
|
||||||
channel: appVersionInfo.channel,
|
|
||||||
version: appVersionInfo.version
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
sharedViews.FeedbackView({
|
sharedViews.FeedbackView({
|
||||||
feedbackApiClient: feedbackClient,
|
feedbackStore: this.props.feedbackStore,
|
||||||
onAfterFeedbackReceived: this._closeWindow.bind(this)}
|
onAfterFeedbackReceived: this._closeWindow.bind(this)}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -356,7 +356,7 @@ loop.conversationViews = (function(mozL10n) {
|
||||||
nameDisplayMode: "off",
|
nameDisplayMode: "off",
|
||||||
videoDisabledDisplayMode: "off"
|
videoDisabledDisplayMode: "off"
|
||||||
}
|
}
|
||||||
}
|
};
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@ -431,7 +431,8 @@ loop.conversationViews = (function(mozL10n) {
|
||||||
propTypes: {
|
propTypes: {
|
||||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||||
store: React.PropTypes.instanceOf(
|
store: React.PropTypes.instanceOf(
|
||||||
loop.store.ConversationStore).isRequired
|
loop.store.ConversationStore).isRequired,
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -462,22 +463,9 @@ loop.conversationViews = (function(mozL10n) {
|
||||||
_renderFeedbackView: function() {
|
_renderFeedbackView: function() {
|
||||||
document.title = mozL10n.get("conversation_has_ended");
|
document.title = mozL10n.get("conversation_has_ended");
|
||||||
|
|
||||||
// XXX Bug 1076754 Feedback view should be redone in the Flux style.
|
|
||||||
var feebackAPIBaseUrl = navigator.mozLoop.getLoopPref(
|
|
||||||
"feedback.baseUrl");
|
|
||||||
|
|
||||||
var appVersionInfo = navigator.mozLoop.appVersionInfo;
|
|
||||||
|
|
||||||
var feedbackClient = new loop.FeedbackAPIClient(feebackAPIBaseUrl, {
|
|
||||||
product: navigator.mozLoop.getLoopPref("feedback.product"),
|
|
||||||
platform: appVersionInfo.OS,
|
|
||||||
channel: appVersionInfo.channel,
|
|
||||||
version: appVersionInfo.version
|
|
||||||
});
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<sharedViews.FeedbackView
|
<sharedViews.FeedbackView
|
||||||
feedbackApiClient={feedbackClient}
|
feedbackStore={this.props.feedbackStore}
|
||||||
onAfterFeedbackReceived={this._closeWindow.bind(this)}
|
onAfterFeedbackReceived={this._closeWindow.bind(this)}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|
|
@ -281,6 +281,13 @@ loop.panel = (function(_, mozL10n) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleHelpEntry: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var helloSupportUrl = navigator.mozLoop.getLoopPref('support_url');
|
||||||
|
window.open(helloSupportUrl);
|
||||||
|
window.close();
|
||||||
|
},
|
||||||
|
|
||||||
_isSignedIn: function() {
|
_isSignedIn: function() {
|
||||||
return !!navigator.mozLoop.userProfile;
|
return !!navigator.mozLoop.userProfile;
|
||||||
},
|
},
|
||||||
|
@ -318,7 +325,10 @@ loop.panel = (function(_, mozL10n) {
|
||||||
mozL10n.get("settings_menu_item_signin"),
|
mozL10n.get("settings_menu_item_signin"),
|
||||||
onClick: this.handleClickAuthEntry,
|
onClick: this.handleClickAuthEntry,
|
||||||
displayed: navigator.mozLoop.fxAEnabled,
|
displayed: navigator.mozLoop.fxAEnabled,
|
||||||
icon: this._isSignedIn() ? "signout" : "signin"})
|
icon: this._isSignedIn() ? "signout" : "signin"}),
|
||||||
|
SettingsDropdownEntry({label: mozL10n.get("help_label"),
|
||||||
|
onClick: this.handleHelpEntry,
|
||||||
|
icon: "help"})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
|
|
@ -281,6 +281,13 @@ loop.panel = (function(_, mozL10n) {
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
|
||||||
|
handleHelpEntry: function(event) {
|
||||||
|
event.preventDefault();
|
||||||
|
var helloSupportUrl = navigator.mozLoop.getLoopPref('support_url');
|
||||||
|
window.open(helloSupportUrl);
|
||||||
|
window.close();
|
||||||
|
},
|
||||||
|
|
||||||
_isSignedIn: function() {
|
_isSignedIn: function() {
|
||||||
return !!navigator.mozLoop.userProfile;
|
return !!navigator.mozLoop.userProfile;
|
||||||
},
|
},
|
||||||
|
@ -319,6 +326,9 @@ loop.panel = (function(_, mozL10n) {
|
||||||
onClick={this.handleClickAuthEntry}
|
onClick={this.handleClickAuthEntry}
|
||||||
displayed={navigator.mozLoop.fxAEnabled}
|
displayed={navigator.mozLoop.fxAEnabled}
|
||||||
icon={this._isSignedIn() ? "signout" : "signin"} />
|
icon={this._isSignedIn() ? "signout" : "signin"} />
|
||||||
|
<SettingsDropdownEntry label={mozL10n.get("help_label")}
|
||||||
|
onClick={this.handleHelpEntry}
|
||||||
|
icon="help" />
|
||||||
</ul>
|
</ul>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
|
|
|
@ -706,6 +706,7 @@ html, .fx-embedded, #main,
|
||||||
background: #000;
|
background: #000;
|
||||||
height: 50px;
|
height: 50px;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
width: 75%;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-conversation-wrapper header h1 {
|
.room-conversation-wrapper header h1 {
|
||||||
|
@ -717,6 +718,20 @@ html, .fx-embedded, #main,
|
||||||
background-size: 30px;
|
background-size: 30px;
|
||||||
background-position: 10px;
|
background-position: 10px;
|
||||||
background-repeat: no-repeat;
|
background-repeat: no-repeat;
|
||||||
|
display: inline-block;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-conversation-wrapper header a {
|
||||||
|
float: right;
|
||||||
|
}
|
||||||
|
|
||||||
|
.room-conversation-wrapper header .icon-help {
|
||||||
|
display: inline-block;
|
||||||
|
background-size: contain;
|
||||||
|
margin-top: 20px;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
background: transparent url("../img/svg/glyph-help-16x16.svg") no-repeat;
|
||||||
}
|
}
|
||||||
|
|
||||||
.room-conversation-wrapper footer {
|
.room-conversation-wrapper footer {
|
||||||
|
|
|
@ -659,6 +659,10 @@ body[dir=rtl] .generate-url-spinner {
|
||||||
background: transparent url(../img/svg/glyph-signout-16x16.svg) no-repeat center center;
|
background: transparent url(../img/svg/glyph-signout-16x16.svg) no-repeat center center;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.settings-menu .icon-help {
|
||||||
|
background: transparent url(../img/svg/glyph-help-16x16.svg) no-repeat center center;
|
||||||
|
}
|
||||||
|
|
||||||
/* Footer */
|
/* Footer */
|
||||||
|
|
||||||
.footer {
|
.footer {
|
||||||
|
|
|
@ -0,0 +1,15 @@
|
||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<!-- 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/. -->
|
||||||
|
<!-- Generator: Adobe Illustrator 18.1.0, SVG Export Plug-In . SVG Version: 6.00 Build 0) -->
|
||||||
|
<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
|
||||||
|
viewBox="0 0 16 16" enable-background="new 0 0 16 16" xml:space="preserve">
|
||||||
|
<circle fill="#5A5A5A" cx="8" cy="8" r="8"/>
|
||||||
|
<g>
|
||||||
|
<path fill="#FFFFFF" d="M10.716,5.643c0,1.943-2.158,1.812-2.158,3.154v0.3H6.831V8.726c0-2.075,1.907-1.932,1.907-2.915
|
||||||
|
c0-0.432-0.312-0.684-0.84-0.684c-0.491,0-0.972,0.24-1.403,0.731L5.284,4.923C5.967,4.121,6.855,3.64,8.09,3.64
|
||||||
|
C9.841,3.64,10.716,4.576,10.716,5.643z M8.797,11.268c0,0.6-0.479,1.092-1.079,1.092s-1.079-0.492-1.079-1.092
|
||||||
|
c0-0.588,0.479-1.079,1.079-1.079S8.797,10.68,8.797,11.268z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
После Ширина: | Высота: | Размер: 1.0 KiB |
|
@ -325,6 +325,28 @@ loop.shared.actions = (function() {
|
||||||
* Used to indicate the user wishes to leave the room.
|
* Used to indicate the user wishes to leave the room.
|
||||||
*/
|
*/
|
||||||
LeaveRoom: Action.define("leaveRoom", {
|
LeaveRoom: Action.define("leaveRoom", {
|
||||||
|
}),
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
||||||
})
|
})
|
||||||
};
|
};
|
||||||
})();
|
})();
|
||||||
|
|
|
@ -107,8 +107,8 @@ loop.FeedbackAPIClient = (function($, _) {
|
||||||
req.fail(function(jqXHR, textStatus, errorThrown) {
|
req.fail(function(jqXHR, textStatus, errorThrown) {
|
||||||
var message = "Error posting user feedback data";
|
var message = "Error posting user feedback data";
|
||||||
var httpError = jqXHR.status + " " + errorThrown;
|
var httpError = jqXHR.status + " " + errorThrown;
|
||||||
console.error(message, httpError, JSON.stringify(jqXHR.responseJSON));
|
cb(new Error(message + ": " + httpError + "; " +
|
||||||
cb(new Error(message + ": " + httpError));
|
(jqXHR.responseJSON && jqXHR.responseJSON.detail || "")));
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
|
@ -0,0 +1,98 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/* global loop:true */
|
||||||
|
|
||||||
|
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"
|
||||||
|
],
|
||||||
|
|
||||||
|
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
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FeedbackStore;
|
||||||
|
})();
|
|
@ -0,0 +1,326 @@
|
||||||
|
/** @jsx React.DOM */
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* jshint newcap:false */
|
||||||
|
/* global loop:true, React */
|
||||||
|
var loop = loop || {};
|
||||||
|
loop.shared = loop.shared || {};
|
||||||
|
loop.shared.views = loop.shared.views || {};
|
||||||
|
loop.shared.views.FeedbackView = (function(l10n) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var sharedActions = loop.shared.actions;
|
||||||
|
var sharedMixins = loop.shared.mixins;
|
||||||
|
|
||||||
|
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
||||||
|
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback outer layout.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* -
|
||||||
|
*/
|
||||||
|
var FeedbackLayout = React.createClass({displayName: 'FeedbackLayout',
|
||||||
|
propTypes: {
|
||||||
|
children: React.PropTypes.component.isRequired,
|
||||||
|
title: React.PropTypes.string.isRequired,
|
||||||
|
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var backButton = React.DOM.div(null);
|
||||||
|
if (this.props.reset) {
|
||||||
|
backButton = (
|
||||||
|
React.DOM.button({className: "fx-embedded-btn-back", type: "button",
|
||||||
|
onClick: this.props.reset},
|
||||||
|
"« ", l10n.get("feedback_back_button")
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
React.DOM.div({className: "feedback"},
|
||||||
|
backButton,
|
||||||
|
React.DOM.h3(null, this.props.title),
|
||||||
|
this.props.children
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detailed feedback form.
|
||||||
|
*/
|
||||||
|
var FeedbackForm = React.createClass({displayName: 'FeedbackForm',
|
||||||
|
propTypes: {
|
||||||
|
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_confusing"),
|
||||||
|
other: l10n.get("feedback_category_other")
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_getCategoryFields: function() {
|
||||||
|
var categories = this._getCategories();
|
||||||
|
return Object.keys(categories).map(function(category, key) {
|
||||||
|
return (
|
||||||
|
React.DOM.label({key: key, className: "feedback-category-label"},
|
||||||
|
React.DOM.input({type: "radio", ref: "category", name: "category",
|
||||||
|
className: "feedback-category-radio",
|
||||||
|
value: category,
|
||||||
|
onChange: this.handleCategoryChange,
|
||||||
|
checked: this.state.category === category}),
|
||||||
|
categories[category]
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}, this);
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Checks if the form is ready for submission:
|
||||||
|
*
|
||||||
|
* - no feedback submission should be pending.
|
||||||
|
* - a category (reason) must be chosen;
|
||||||
|
* - if the "other" category is chosen, a custom description must have been
|
||||||
|
* entered by the end user;
|
||||||
|
*
|
||||||
|
* @return {Boolean}
|
||||||
|
*/
|
||||||
|
_isFormReady: function() {
|
||||||
|
if (this.props.pending || !this.state.category) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (this.state.category === "other" && !this.state.description) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
},
|
||||||
|
|
||||||
|
handleCategoryChange: function(event) {
|
||||||
|
var category = event.target.value;
|
||||||
|
this.setState({
|
||||||
|
category: category,
|
||||||
|
description: category == "other" ? "" : this._getCategories()[category]
|
||||||
|
});
|
||||||
|
if (category == "other") {
|
||||||
|
this.refs.description.getDOMNode().focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDescriptionFieldChange: function(event) {
|
||||||
|
this.setState({description: event.target.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDescriptionFieldFocus: function(event) {
|
||||||
|
this.setState({category: "other", description: ""});
|
||||||
|
},
|
||||||
|
|
||||||
|
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() {
|
||||||
|
var descriptionDisplayValue = this.state.category === "other" ?
|
||||||
|
this.state.description : "";
|
||||||
|
return (
|
||||||
|
FeedbackLayout({title: l10n.get("feedback_what_makes_you_sad"),
|
||||||
|
reset: this.props.reset},
|
||||||
|
React.DOM.form({onSubmit: this.handleFormSubmit},
|
||||||
|
this._getCategoryFields(),
|
||||||
|
React.DOM.p(null,
|
||||||
|
React.DOM.input({type: "text", ref: "description", name: "description",
|
||||||
|
className: "feedback-description",
|
||||||
|
onChange: this.handleDescriptionFieldChange,
|
||||||
|
onFocus: this.handleDescriptionFieldFocus,
|
||||||
|
value: descriptionDisplayValue,
|
||||||
|
placeholder:
|
||||||
|
l10n.get("feedback_custom_category_text_placeholder")})
|
||||||
|
),
|
||||||
|
React.DOM.button({type: "submit", className: "btn btn-success",
|
||||||
|
disabled: !this._isFormReady()},
|
||||||
|
l10n.get("feedback_submit_button")
|
||||||
|
)
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback received view.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* - {Function} onAfterFeedbackReceived Function to execute after the
|
||||||
|
* WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
|
||||||
|
*/
|
||||||
|
var FeedbackReceived = React.createClass({displayName: 'FeedbackReceived',
|
||||||
|
propTypes: {
|
||||||
|
onAfterFeedbackReceived: React.PropTypes.func
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this._timer = setInterval(function() {
|
||||||
|
this.setState({countdown: this.state.countdown - 1});
|
||||||
|
}.bind(this), 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
if (this._timer) {
|
||||||
|
clearInterval(this._timer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.countdown < 1) {
|
||||||
|
clearInterval(this._timer);
|
||||||
|
if (this.props.onAfterFeedbackReceived) {
|
||||||
|
this.props.onAfterFeedbackReceived();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
FeedbackLayout({title: l10n.get("feedback_thank_you_heading")},
|
||||||
|
React.DOM.p({className: "info thank-you"},
|
||||||
|
l10n.get("feedback_window_will_close_in2", {
|
||||||
|
countdown: this.state.countdown,
|
||||||
|
num: this.state.countdown
|
||||||
|
}))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback view.
|
||||||
|
*/
|
||||||
|
var FeedbackView = React.createClass({displayName: 'FeedbackView',
|
||||||
|
mixins: [Backbone.Events, sharedMixins.AudioMixin],
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
|
||||||
|
onAfterFeedbackReceived: React.PropTypes.func,
|
||||||
|
// Used by the UI showcase.
|
||||||
|
feedbackState: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
var storeState = this.props.feedbackStore.getStoreState();
|
||||||
|
return _.extend({}, storeState, {
|
||||||
|
feedbackState: this.props.feedbackState || storeState.feedbackState
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.listenTo(this.props.feedbackStore, "change", this._onStoreStateChanged);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.play("terminated");
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this.stopListening(this.props.feedbackStore);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onStoreStateChanged: function() {
|
||||||
|
this.setState(this.props.feedbackStore.getStoreState());
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: function() {
|
||||||
|
this.setState(this.props.feedbackStore.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.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
|
||||||
|
happy: true,
|
||||||
|
category: "",
|
||||||
|
description: ""
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSadClick: function() {
|
||||||
|
this.props.feedbackStore.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")},
|
||||||
|
React.DOM.div({className: "faces"},
|
||||||
|
React.DOM.button({className: "face face-happy",
|
||||||
|
onClick: this.handleHappyClick}),
|
||||||
|
React.DOM.button({className: "face face-sad",
|
||||||
|
onClick: this.handleSadClick})
|
||||||
|
)
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case FEEDBACK_STATES.DETAILS: {
|
||||||
|
return (
|
||||||
|
FeedbackForm({
|
||||||
|
feedbackStore: this.props.feedbackStore,
|
||||||
|
reset: this.reset,
|
||||||
|
pending: this.state.feedbackState === FEEDBACK_STATES.PENDING})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case FEEDBACK_STATES.PENDING:
|
||||||
|
case FEEDBACK_STATES.SENT:
|
||||||
|
case FEEDBACK_STATES.FAILED: {
|
||||||
|
if (this.state.error) {
|
||||||
|
// XXX better end user error reporting, see bug 1046738
|
||||||
|
console.error("Error encountered while submitting feedback",
|
||||||
|
this.state.error);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
FeedbackReceived({
|
||||||
|
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FeedbackView;
|
||||||
|
})(navigator.mozL10n || document.mozL10n);
|
|
@ -0,0 +1,326 @@
|
||||||
|
/** @jsx React.DOM */
|
||||||
|
|
||||||
|
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||||
|
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||||
|
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||||
|
|
||||||
|
/* jshint newcap:false */
|
||||||
|
/* global loop:true, React */
|
||||||
|
var loop = loop || {};
|
||||||
|
loop.shared = loop.shared || {};
|
||||||
|
loop.shared.views = loop.shared.views || {};
|
||||||
|
loop.shared.views.FeedbackView = (function(l10n) {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var sharedActions = loop.shared.actions;
|
||||||
|
var sharedMixins = loop.shared.mixins;
|
||||||
|
|
||||||
|
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
||||||
|
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback outer layout.
|
||||||
|
*
|
||||||
|
* Props:
|
||||||
|
* -
|
||||||
|
*/
|
||||||
|
var FeedbackLayout = React.createClass({
|
||||||
|
propTypes: {
|
||||||
|
children: React.PropTypes.component.isRequired,
|
||||||
|
title: React.PropTypes.string.isRequired,
|
||||||
|
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
var backButton = <div />;
|
||||||
|
if (this.props.reset) {
|
||||||
|
backButton = (
|
||||||
|
<button className="fx-embedded-btn-back" type="button"
|
||||||
|
onClick={this.props.reset}>
|
||||||
|
« {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_confusing"),
|
||||||
|
other: l10n.get("feedback_category_other")
|
||||||
|
};
|
||||||
|
},
|
||||||
|
|
||||||
|
_getCategoryFields: function() {
|
||||||
|
var categories = this._getCategories();
|
||||||
|
return Object.keys(categories).map(function(category, key) {
|
||||||
|
return (
|
||||||
|
<label key={key} className="feedback-category-label">
|
||||||
|
<input type="radio" ref="category" name="category"
|
||||||
|
className="feedback-category-radio"
|
||||||
|
value={category}
|
||||||
|
onChange={this.handleCategoryChange}
|
||||||
|
checked={this.state.category === 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,
|
||||||
|
description: category == "other" ? "" : this._getCategories()[category]
|
||||||
|
});
|
||||||
|
if (category == "other") {
|
||||||
|
this.refs.description.getDOMNode().focus();
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDescriptionFieldChange: function(event) {
|
||||||
|
this.setState({description: event.target.value});
|
||||||
|
},
|
||||||
|
|
||||||
|
handleDescriptionFieldFocus: function(event) {
|
||||||
|
this.setState({category: "other", description: ""});
|
||||||
|
},
|
||||||
|
|
||||||
|
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() {
|
||||||
|
var descriptionDisplayValue = this.state.category === "other" ?
|
||||||
|
this.state.description : "";
|
||||||
|
return (
|
||||||
|
<FeedbackLayout title={l10n.get("feedback_what_makes_you_sad")}
|
||||||
|
reset={this.props.reset}>
|
||||||
|
<form onSubmit={this.handleFormSubmit}>
|
||||||
|
{this._getCategoryFields()}
|
||||||
|
<p>
|
||||||
|
<input type="text" ref="description" name="description"
|
||||||
|
className="feedback-description"
|
||||||
|
onChange={this.handleDescriptionFieldChange}
|
||||||
|
onFocus={this.handleDescriptionFieldFocus}
|
||||||
|
value={descriptionDisplayValue}
|
||||||
|
placeholder={
|
||||||
|
l10n.get("feedback_custom_category_text_placeholder")} />
|
||||||
|
</p>
|
||||||
|
<button type="submit" className="btn btn-success"
|
||||||
|
disabled={!this._isFormReady()}>
|
||||||
|
{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: {
|
||||||
|
onAfterFeedbackReceived: React.PropTypes.func
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this._timer = setInterval(function() {
|
||||||
|
this.setState({countdown: this.state.countdown - 1});
|
||||||
|
}.bind(this), 1000);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
if (this._timer) {
|
||||||
|
clearInterval(this._timer);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
|
||||||
|
render: function() {
|
||||||
|
if (this.state.countdown < 1) {
|
||||||
|
clearInterval(this._timer);
|
||||||
|
if (this.props.onAfterFeedbackReceived) {
|
||||||
|
this.props.onAfterFeedbackReceived();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
|
||||||
|
<p className="info thank-you">{
|
||||||
|
l10n.get("feedback_window_will_close_in2", {
|
||||||
|
countdown: this.state.countdown,
|
||||||
|
num: this.state.countdown
|
||||||
|
})}</p>
|
||||||
|
</FeedbackLayout>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Feedback view.
|
||||||
|
*/
|
||||||
|
var FeedbackView = React.createClass({
|
||||||
|
mixins: [Backbone.Events, sharedMixins.AudioMixin],
|
||||||
|
|
||||||
|
propTypes: {
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
|
||||||
|
onAfterFeedbackReceived: React.PropTypes.func,
|
||||||
|
// Used by the UI showcase.
|
||||||
|
feedbackState: React.PropTypes.string
|
||||||
|
},
|
||||||
|
|
||||||
|
getInitialState: function() {
|
||||||
|
var storeState = this.props.feedbackStore.getStoreState();
|
||||||
|
return _.extend({}, storeState, {
|
||||||
|
feedbackState: this.props.feedbackState || storeState.feedbackState
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillMount: function() {
|
||||||
|
this.listenTo(this.props.feedbackStore, "change", this._onStoreStateChanged);
|
||||||
|
},
|
||||||
|
|
||||||
|
componentDidMount: function() {
|
||||||
|
this.play("terminated");
|
||||||
|
},
|
||||||
|
|
||||||
|
componentWillUnmount: function() {
|
||||||
|
this.stopListening(this.props.feedbackStore);
|
||||||
|
},
|
||||||
|
|
||||||
|
_onStoreStateChanged: function() {
|
||||||
|
this.setState(this.props.feedbackStore.getStoreState());
|
||||||
|
},
|
||||||
|
|
||||||
|
reset: function() {
|
||||||
|
this.setState(this.props.feedbackStore.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.props.feedbackStore.dispatchAction(new sharedActions.SendFeedback({
|
||||||
|
happy: true,
|
||||||
|
category: "",
|
||||||
|
description: ""
|
||||||
|
}));
|
||||||
|
},
|
||||||
|
|
||||||
|
handleSadClick: function() {
|
||||||
|
this.props.feedbackStore.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.props.feedbackStore}
|
||||||
|
reset={this.reset}
|
||||||
|
pending={this.state.feedbackState === FEEDBACK_STATES.PENDING} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
case FEEDBACK_STATES.PENDING:
|
||||||
|
case FEEDBACK_STATES.SENT:
|
||||||
|
case FEEDBACK_STATES.FAILED: {
|
||||||
|
if (this.state.error) {
|
||||||
|
// XXX better end user error reporting, see bug 1046738
|
||||||
|
console.error("Error encountered while submitting feedback",
|
||||||
|
this.state.error);
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<FeedbackReceived
|
||||||
|
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
return FeedbackView;
|
||||||
|
})(navigator.mozL10n || document.mozL10n);
|
|
@ -14,8 +14,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
var sharedModels = loop.shared.models;
|
var sharedModels = loop.shared.models;
|
||||||
var sharedMixins = loop.shared.mixins;
|
var sharedMixins = loop.shared.mixins;
|
||||||
|
|
||||||
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media control button.
|
* Media control button.
|
||||||
*
|
*
|
||||||
|
@ -345,287 +343,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Feedback outer layout.
|
|
||||||
*
|
|
||||||
* Props:
|
|
||||||
* -
|
|
||||||
*/
|
|
||||||
var FeedbackLayout = React.createClass({displayName: 'FeedbackLayout',
|
|
||||||
propTypes: {
|
|
||||||
children: React.PropTypes.component.isRequired,
|
|
||||||
title: React.PropTypes.string.isRequired,
|
|
||||||
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var backButton = React.DOM.div(null);
|
|
||||||
if (this.props.reset) {
|
|
||||||
backButton = (
|
|
||||||
React.DOM.button({className: "fx-embedded-btn-back", type: "button",
|
|
||||||
onClick: this.props.reset},
|
|
||||||
"« ", l10n.get("feedback_back_button")
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
React.DOM.div({className: "feedback"},
|
|
||||||
backButton,
|
|
||||||
React.DOM.h3(null, this.props.title),
|
|
||||||
this.props.children
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Detailed feedback form.
|
|
||||||
*/
|
|
||||||
var FeedbackForm = React.createClass({displayName: 'FeedbackForm',
|
|
||||||
propTypes: {
|
|
||||||
pending: React.PropTypes.bool,
|
|
||||||
sendFeedback: React.PropTypes.func,
|
|
||||||
reset: React.PropTypes.func
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {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_confusing"),
|
|
||||||
other: l10n.get("feedback_category_other")
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
_getCategoryFields: function() {
|
|
||||||
var categories = this._getCategories();
|
|
||||||
return Object.keys(categories).map(function(category, key) {
|
|
||||||
return (
|
|
||||||
React.DOM.label({key: key, className: "feedback-category-label"},
|
|
||||||
React.DOM.input({type: "radio", ref: "category", name: "category",
|
|
||||||
className: "feedback-category-radio",
|
|
||||||
value: category,
|
|
||||||
onChange: this.handleCategoryChange,
|
|
||||||
checked: this.state.category === category}),
|
|
||||||
categories[category]
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}, this);
|
|
||||||
},
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks if the form is ready for submission:
|
|
||||||
*
|
|
||||||
* - no feedback submission should be pending.
|
|
||||||
* - a category (reason) must be chosen;
|
|
||||||
* - if the "other" category is chosen, a custom description must have been
|
|
||||||
* entered by the end user;
|
|
||||||
*
|
|
||||||
* @return {Boolean}
|
|
||||||
*/
|
|
||||||
_isFormReady: function() {
|
|
||||||
if (this.props.pending || !this.state.category) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
if (this.state.category === "other" && !this.state.description) {
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
return true;
|
|
||||||
},
|
|
||||||
|
|
||||||
handleCategoryChange: function(event) {
|
|
||||||
var category = event.target.value;
|
|
||||||
this.setState({
|
|
||||||
category: category,
|
|
||||||
description: category == "other" ? "" : this._getCategories()[category]
|
|
||||||
});
|
|
||||||
if (category == "other") {
|
|
||||||
this.refs.description.getDOMNode().focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleDescriptionFieldChange: function(event) {
|
|
||||||
this.setState({description: event.target.value});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleDescriptionFieldFocus: function(event) {
|
|
||||||
this.setState({category: "other", description: ""});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFormSubmit: function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.props.sendFeedback({
|
|
||||||
happy: false,
|
|
||||||
category: this.state.category,
|
|
||||||
description: this.state.description
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var descriptionDisplayValue = this.state.category === "other" ?
|
|
||||||
this.state.description : "";
|
|
||||||
return (
|
|
||||||
FeedbackLayout({title: l10n.get("feedback_what_makes_you_sad"),
|
|
||||||
reset: this.props.reset},
|
|
||||||
React.DOM.form({onSubmit: this.handleFormSubmit},
|
|
||||||
this._getCategoryFields(),
|
|
||||||
React.DOM.p(null,
|
|
||||||
React.DOM.input({type: "text", ref: "description", name: "description",
|
|
||||||
className: "feedback-description",
|
|
||||||
onChange: this.handleDescriptionFieldChange,
|
|
||||||
onFocus: this.handleDescriptionFieldFocus,
|
|
||||||
value: descriptionDisplayValue,
|
|
||||||
placeholder:
|
|
||||||
l10n.get("feedback_custom_category_text_placeholder")})
|
|
||||||
),
|
|
||||||
React.DOM.button({type: "submit", className: "btn btn-success",
|
|
||||||
disabled: !this._isFormReady()},
|
|
||||||
l10n.get("feedback_submit_button")
|
|
||||||
)
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feedback received view.
|
|
||||||
*
|
|
||||||
* Props:
|
|
||||||
* - {Function} onAfterFeedbackReceived Function to execute after the
|
|
||||||
* WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS timeout has elapsed
|
|
||||||
*/
|
|
||||||
var FeedbackReceived = React.createClass({displayName: 'FeedbackReceived',
|
|
||||||
propTypes: {
|
|
||||||
onAfterFeedbackReceived: React.PropTypes.func
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._timer = setInterval(function() {
|
|
||||||
this.setState({countdown: this.state.countdown - 1});
|
|
||||||
}.bind(this), 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this._timer) {
|
|
||||||
clearInterval(this._timer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
if (this.state.countdown < 1) {
|
|
||||||
clearInterval(this._timer);
|
|
||||||
if (this.props.onAfterFeedbackReceived) {
|
|
||||||
this.props.onAfterFeedbackReceived();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
FeedbackLayout({title: l10n.get("feedback_thank_you_heading")},
|
|
||||||
React.DOM.p({className: "info thank-you"},
|
|
||||||
l10n.get("feedback_window_will_close_in2", {
|
|
||||||
countdown: this.state.countdown,
|
|
||||||
num: this.state.countdown
|
|
||||||
}))
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feedback view.
|
|
||||||
*/
|
|
||||||
var FeedbackView = React.createClass({displayName: 'FeedbackView',
|
|
||||||
mixins: [sharedMixins.AudioMixin],
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
// A loop.FeedbackAPIClient instance
|
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
|
||||||
onAfterFeedbackReceived: React.PropTypes.func,
|
|
||||||
// The current feedback submission flow step name
|
|
||||||
step: React.PropTypes.oneOf(["start", "form", "finished"])
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {pending: false, step: this.props.step || "start"};
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {step: "start"};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.play("terminated");
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
|
||||||
this.setState(this.getInitialState());
|
|
||||||
},
|
|
||||||
|
|
||||||
handleHappyClick: function() {
|
|
||||||
this.sendFeedback({happy: true}, this._onFeedbackSent);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSadClick: function() {
|
|
||||||
this.setState({step: "form"});
|
|
||||||
},
|
|
||||||
|
|
||||||
sendFeedback: function(fields) {
|
|
||||||
// Setting state.pending to true will disable the submit button to avoid
|
|
||||||
// multiple submissions
|
|
||||||
this.setState({pending: true});
|
|
||||||
// Sends feedback data
|
|
||||||
this.props.feedbackApiClient.send(fields, this._onFeedbackSent);
|
|
||||||
},
|
|
||||||
|
|
||||||
_onFeedbackSent: function(err) {
|
|
||||||
if (err) {
|
|
||||||
// XXX better end user error reporting, see bug 1046738
|
|
||||||
console.error("Unable to send user feedback", err);
|
|
||||||
}
|
|
||||||
this.setState({pending: false, step: "finished"});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
switch(this.state.step) {
|
|
||||||
case "finished":
|
|
||||||
return (
|
|
||||||
FeedbackReceived({
|
|
||||||
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
|
|
||||||
);
|
|
||||||
case "form":
|
|
||||||
return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient,
|
|
||||||
sendFeedback: this.sendFeedback,
|
|
||||||
reset: this.reset,
|
|
||||||
pending: this.state.pending});
|
|
||||||
default:
|
|
||||||
return (
|
|
||||||
FeedbackLayout({title:
|
|
||||||
l10n.get("feedback_call_experience_heading2")},
|
|
||||||
React.DOM.div({className: "faces"},
|
|
||||||
React.DOM.button({className: "face face-happy",
|
|
||||||
onClick: this.handleHappyClick}),
|
|
||||||
React.DOM.button({className: "face face-sad",
|
|
||||||
onClick: this.handleSadClick})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification view.
|
* Notification view.
|
||||||
*/
|
*/
|
||||||
|
@ -743,7 +460,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
React.DOM.span({className: "button-caption"}, this.props.caption),
|
React.DOM.span({className: "button-caption"}, this.props.caption),
|
||||||
this.props.children
|
this.props.children
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -768,7 +485,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
React.DOM.div({className: cx(classObject)},
|
React.DOM.div({className: cx(classObject)},
|
||||||
this.props.children
|
this.props.children
|
||||||
)
|
)
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -777,7 +494,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
ButtonGroup: ButtonGroup,
|
ButtonGroup: ButtonGroup,
|
||||||
ConversationView: ConversationView,
|
ConversationView: ConversationView,
|
||||||
ConversationToolbar: ConversationToolbar,
|
ConversationToolbar: ConversationToolbar,
|
||||||
FeedbackView: FeedbackView,
|
|
||||||
MediaControlButton: MediaControlButton,
|
MediaControlButton: MediaControlButton,
|
||||||
NotificationListView: NotificationListView
|
NotificationListView: NotificationListView
|
||||||
};
|
};
|
||||||
|
|
|
@ -14,8 +14,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
var sharedModels = loop.shared.models;
|
var sharedModels = loop.shared.models;
|
||||||
var sharedMixins = loop.shared.mixins;
|
var sharedMixins = loop.shared.mixins;
|
||||||
|
|
||||||
var WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS = 5;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Media control button.
|
* Media control button.
|
||||||
*
|
*
|
||||||
|
@ -345,287 +343,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
/**
|
|
||||||
* Feedback outer layout.
|
|
||||||
*
|
|
||||||
* Props:
|
|
||||||
* -
|
|
||||||
*/
|
|
||||||
var FeedbackLayout = React.createClass({
|
|
||||||
propTypes: {
|
|
||||||
children: React.PropTypes.component.isRequired,
|
|
||||||
title: React.PropTypes.string.isRequired,
|
|
||||||
reset: React.PropTypes.func // if not specified, no Back btn is shown
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var backButton = <div />;
|
|
||||||
if (this.props.reset) {
|
|
||||||
backButton = (
|
|
||||||
<button className="fx-embedded-btn-back" type="button"
|
|
||||||
onClick={this.props.reset}>
|
|
||||||
« {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: {
|
|
||||||
pending: React.PropTypes.bool,
|
|
||||||
sendFeedback: React.PropTypes.func,
|
|
||||||
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_confusing"),
|
|
||||||
other: l10n.get("feedback_category_other")
|
|
||||||
};
|
|
||||||
},
|
|
||||||
|
|
||||||
_getCategoryFields: function() {
|
|
||||||
var categories = this._getCategories();
|
|
||||||
return Object.keys(categories).map(function(category, key) {
|
|
||||||
return (
|
|
||||||
<label key={key} className="feedback-category-label">
|
|
||||||
<input type="radio" ref="category" name="category"
|
|
||||||
className="feedback-category-radio"
|
|
||||||
value={category}
|
|
||||||
onChange={this.handleCategoryChange}
|
|
||||||
checked={this.state.category === 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,
|
|
||||||
description: category == "other" ? "" : this._getCategories()[category]
|
|
||||||
});
|
|
||||||
if (category == "other") {
|
|
||||||
this.refs.description.getDOMNode().focus();
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
handleDescriptionFieldChange: function(event) {
|
|
||||||
this.setState({description: event.target.value});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleDescriptionFieldFocus: function(event) {
|
|
||||||
this.setState({category: "other", description: ""});
|
|
||||||
},
|
|
||||||
|
|
||||||
handleFormSubmit: function(event) {
|
|
||||||
event.preventDefault();
|
|
||||||
this.props.sendFeedback({
|
|
||||||
happy: false,
|
|
||||||
category: this.state.category,
|
|
||||||
description: this.state.description
|
|
||||||
});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
var descriptionDisplayValue = this.state.category === "other" ?
|
|
||||||
this.state.description : "";
|
|
||||||
return (
|
|
||||||
<FeedbackLayout title={l10n.get("feedback_what_makes_you_sad")}
|
|
||||||
reset={this.props.reset}>
|
|
||||||
<form onSubmit={this.handleFormSubmit}>
|
|
||||||
{this._getCategoryFields()}
|
|
||||||
<p>
|
|
||||||
<input type="text" ref="description" name="description"
|
|
||||||
className="feedback-description"
|
|
||||||
onChange={this.handleDescriptionFieldChange}
|
|
||||||
onFocus={this.handleDescriptionFieldFocus}
|
|
||||||
value={descriptionDisplayValue}
|
|
||||||
placeholder={
|
|
||||||
l10n.get("feedback_custom_category_text_placeholder")} />
|
|
||||||
</p>
|
|
||||||
<button type="submit" className="btn btn-success"
|
|
||||||
disabled={!this._isFormReady()}>
|
|
||||||
{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: {
|
|
||||||
onAfterFeedbackReceived: React.PropTypes.func
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {countdown: WINDOW_AUTOCLOSE_TIMEOUT_IN_SECONDS};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this._timer = setInterval(function() {
|
|
||||||
this.setState({countdown: this.state.countdown - 1});
|
|
||||||
}.bind(this), 1000);
|
|
||||||
},
|
|
||||||
|
|
||||||
componentWillUnmount: function() {
|
|
||||||
if (this._timer) {
|
|
||||||
clearInterval(this._timer);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
if (this.state.countdown < 1) {
|
|
||||||
clearInterval(this._timer);
|
|
||||||
if (this.props.onAfterFeedbackReceived) {
|
|
||||||
this.props.onAfterFeedbackReceived();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return (
|
|
||||||
<FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
|
|
||||||
<p className="info thank-you">{
|
|
||||||
l10n.get("feedback_window_will_close_in2", {
|
|
||||||
countdown: this.state.countdown,
|
|
||||||
num: this.state.countdown
|
|
||||||
})}</p>
|
|
||||||
</FeedbackLayout>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Feedback view.
|
|
||||||
*/
|
|
||||||
var FeedbackView = React.createClass({
|
|
||||||
mixins: [sharedMixins.AudioMixin],
|
|
||||||
|
|
||||||
propTypes: {
|
|
||||||
// A loop.FeedbackAPIClient instance
|
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
|
||||||
onAfterFeedbackReceived: React.PropTypes.func,
|
|
||||||
// The current feedback submission flow step name
|
|
||||||
step: React.PropTypes.oneOf(["start", "form", "finished"])
|
|
||||||
},
|
|
||||||
|
|
||||||
getInitialState: function() {
|
|
||||||
return {pending: false, step: this.props.step || "start"};
|
|
||||||
},
|
|
||||||
|
|
||||||
getDefaultProps: function() {
|
|
||||||
return {step: "start"};
|
|
||||||
},
|
|
||||||
|
|
||||||
componentDidMount: function() {
|
|
||||||
this.play("terminated");
|
|
||||||
},
|
|
||||||
|
|
||||||
reset: function() {
|
|
||||||
this.setState(this.getInitialState());
|
|
||||||
},
|
|
||||||
|
|
||||||
handleHappyClick: function() {
|
|
||||||
this.sendFeedback({happy: true}, this._onFeedbackSent);
|
|
||||||
},
|
|
||||||
|
|
||||||
handleSadClick: function() {
|
|
||||||
this.setState({step: "form"});
|
|
||||||
},
|
|
||||||
|
|
||||||
sendFeedback: function(fields) {
|
|
||||||
// Setting state.pending to true will disable the submit button to avoid
|
|
||||||
// multiple submissions
|
|
||||||
this.setState({pending: true});
|
|
||||||
// Sends feedback data
|
|
||||||
this.props.feedbackApiClient.send(fields, this._onFeedbackSent);
|
|
||||||
},
|
|
||||||
|
|
||||||
_onFeedbackSent: function(err) {
|
|
||||||
if (err) {
|
|
||||||
// XXX better end user error reporting, see bug 1046738
|
|
||||||
console.error("Unable to send user feedback", err);
|
|
||||||
}
|
|
||||||
this.setState({pending: false, step: "finished"});
|
|
||||||
},
|
|
||||||
|
|
||||||
render: function() {
|
|
||||||
switch(this.state.step) {
|
|
||||||
case "finished":
|
|
||||||
return (
|
|
||||||
<FeedbackReceived
|
|
||||||
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
|
|
||||||
);
|
|
||||||
case "form":
|
|
||||||
return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
|
|
||||||
sendFeedback={this.sendFeedback}
|
|
||||||
reset={this.reset}
|
|
||||||
pending={this.state.pending} />;
|
|
||||||
default:
|
|
||||||
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>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Notification view.
|
* Notification view.
|
||||||
*/
|
*/
|
||||||
|
@ -743,7 +460,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
<span className="button-caption">{this.props.caption}</span>
|
<span className="button-caption">{this.props.caption}</span>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</button>
|
</button>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -768,7 +485,7 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
<div className={cx(classObject)}>
|
<div className={cx(classObject)}>
|
||||||
{this.props.children}
|
{this.props.children}
|
||||||
</div>
|
</div>
|
||||||
)
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -777,7 +494,6 @@ loop.shared.views = (function(_, OT, l10n) {
|
||||||
ButtonGroup: ButtonGroup,
|
ButtonGroup: ButtonGroup,
|
||||||
ConversationView: ConversationView,
|
ConversationView: ConversationView,
|
||||||
ConversationToolbar: ConversationToolbar,
|
ConversationToolbar: ConversationToolbar,
|
||||||
FeedbackView: FeedbackView,
|
|
||||||
MediaControlButton: MediaControlButton,
|
MediaControlButton: MediaControlButton,
|
||||||
NotificationListView: NotificationListView
|
NotificationListView: NotificationListView
|
||||||
};
|
};
|
||||||
|
|
|
@ -50,6 +50,7 @@ browser.jar:
|
||||||
content/browser/loop/shared/img/svg/glyph-account-16x16.svg (content/shared/img/svg/glyph-account-16x16.svg)
|
content/browser/loop/shared/img/svg/glyph-account-16x16.svg (content/shared/img/svg/glyph-account-16x16.svg)
|
||||||
content/browser/loop/shared/img/svg/glyph-signin-16x16.svg (content/shared/img/svg/glyph-signin-16x16.svg)
|
content/browser/loop/shared/img/svg/glyph-signin-16x16.svg (content/shared/img/svg/glyph-signin-16x16.svg)
|
||||||
content/browser/loop/shared/img/svg/glyph-signout-16x16.svg (content/shared/img/svg/glyph-signout-16x16.svg)
|
content/browser/loop/shared/img/svg/glyph-signout-16x16.svg (content/shared/img/svg/glyph-signout-16x16.svg)
|
||||||
|
content/browser/loop/shared/img/svg/glyph-help-16x16.svg (content/shared/img/svg/glyph-help-16x16.svg)
|
||||||
content/browser/loop/shared/img/audio-call-avatar.svg (content/shared/img/audio-call-avatar.svg)
|
content/browser/loop/shared/img/audio-call-avatar.svg (content/shared/img/audio-call-avatar.svg)
|
||||||
content/browser/loop/shared/img/beta-ribbon.svg (content/shared/img/beta-ribbon.svg)
|
content/browser/loop/shared/img/beta-ribbon.svg (content/shared/img/beta-ribbon.svg)
|
||||||
content/browser/loop/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)
|
content/browser/loop/shared/img/icons-10x10.svg (content/shared/img/icons-10x10.svg)
|
||||||
|
@ -70,12 +71,14 @@ browser.jar:
|
||||||
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
|
content/browser/loop/shared/js/store.js (content/shared/js/store.js)
|
||||||
content/browser/loop/shared/js/roomStore.js (content/shared/js/roomStore.js)
|
content/browser/loop/shared/js/roomStore.js (content/shared/js/roomStore.js)
|
||||||
content/browser/loop/shared/js/activeRoomStore.js (content/shared/js/activeRoomStore.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/dispatcher.js (content/shared/js/dispatcher.js)
|
||||||
content/browser/loop/shared/js/feedbackApiClient.js (content/shared/js/feedbackApiClient.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/models.js (content/shared/js/models.js)
|
||||||
content/browser/loop/shared/js/mixins.js (content/shared/js/mixins.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/otSdkDriver.js (content/shared/js/otSdkDriver.js)
|
||||||
content/browser/loop/shared/js/views.js (content/shared/js/views.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/utils.js (content/shared/js/utils.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/validate.js (content/shared/js/validate.js)
|
||||||
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
|
content/browser/loop/shared/js/websocket.js (content/shared/js/websocket.js)
|
||||||
|
|
|
@ -84,3 +84,5 @@ config:
|
||||||
@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
|
@echo "loop.config.fxosApp = loop.config.fxosApp || {};" >> content/config.js
|
||||||
@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
|
@echo "loop.config.fxosApp.name = 'Loop';" >> content/config.js
|
||||||
@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js
|
@echo "loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';" >> content/config.js
|
||||||
|
@echo "loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';" >> content/config.js
|
||||||
|
@echo "loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';" >> content/config.js
|
||||||
|
|
|
@ -87,15 +87,19 @@ body,
|
||||||
color: #777;
|
color: #777;
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-external-links a {
|
.footer-external-links {
|
||||||
padding: .2rem .7rem;
|
padding: .2rem .7rem;
|
||||||
margin: 0 .5rem;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.footer-external-links a:hover {
|
.footer-external-links a {
|
||||||
color: #111;
|
margin: 0 .5rem;
|
||||||
}
|
text-decoration: none;
|
||||||
|
color: #adadad;
|
||||||
|
}
|
||||||
|
|
||||||
|
.footer-external-links a:hover {
|
||||||
|
color: #777;
|
||||||
|
}
|
||||||
|
|
||||||
.footer-logo {
|
.footer-logo {
|
||||||
width: 100px;
|
width: 100px;
|
||||||
|
|
|
@ -99,6 +99,8 @@
|
||||||
<script type="text/javascript" src="shared/js/otSdkDriver.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/store.js"></script>
|
||||||
<script type="text/javascript" src="shared/js/activeRoomStore.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/feedbackViews.js"></script>
|
||||||
<script type="text/javascript" src="js/standaloneAppStore.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/standaloneClient.js"></script>
|
||||||
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
|
<script type="text/javascript" src="js/standaloneMozLoop.js"></script>
|
||||||
|
|
|
@ -107,7 +107,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||||
render: function() {
|
render: function() {
|
||||||
return (
|
return (
|
||||||
React.DOM.header(null,
|
React.DOM.header(null,
|
||||||
React.DOM.h1(null, mozL10n.get("clientShortname2"))
|
React.DOM.h1(null, mozL10n.get("clientShortname2")),
|
||||||
|
React.DOM.a({target: "_blank", href: loop.config.roomsSupportUrl},
|
||||||
|
React.DOM.i({className: "icon icon-help"})
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -108,6 +108,9 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||||
return (
|
return (
|
||||||
<header>
|
<header>
|
||||||
<h1>{mozL10n.get("clientShortname2")}</h1>
|
<h1>{mozL10n.get("clientShortname2")}</h1>
|
||||||
|
<a target="_blank" href={loop.config.roomsSupportUrl}>
|
||||||
|
<i className="icon icon-help"></i>
|
||||||
|
</a>
|
||||||
</header>
|
</header>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
|
@ -259,7 +259,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
React.DOM.div({className: "standalone-footer container-box"},
|
React.DOM.div({className: "standalone-footer container-box"},
|
||||||
React.DOM.div({title: mozL10n.get("vendor_alttext",
|
React.DOM.div({title: mozL10n.get("vendor_alttext",
|
||||||
{vendorShortname: mozL10n.get("vendorShortname")}),
|
{vendorShortname: mozL10n.get("vendorShortname")}),
|
||||||
className: "footer-logo"})
|
className: "footer-logo"}),
|
||||||
|
React.DOM.div({className: "footer-external-links"},
|
||||||
|
React.DOM.a({target: "_blank", href: loop.config.guestSupportUrl},
|
||||||
|
mozL10n.get("support_link")
|
||||||
|
)
|
||||||
|
)
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -538,7 +543,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
|
||||||
onAfterFeedbackReceived: React.PropTypes.func.isRequired
|
onAfterFeedbackReceived: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -549,7 +554,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
return (
|
return (
|
||||||
React.DOM.div({className: "ended-conversation"},
|
React.DOM.div({className: "ended-conversation"},
|
||||||
sharedViews.FeedbackView({
|
sharedViews.FeedbackView({
|
||||||
feedbackApiClient: this.props.feedbackApiClient,
|
feedbackStore: this.props.feedbackStore,
|
||||||
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
|
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
|
||||||
),
|
),
|
||||||
sharedViews.ConversationView({
|
sharedViews.ConversationView({
|
||||||
|
@ -611,7 +616,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -690,7 +695,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
EndedConversationView({
|
EndedConversationView({
|
||||||
sdk: this.props.sdk,
|
sdk: this.props.sdk,
|
||||||
conversation: this.props.conversation,
|
conversation: this.props.conversation,
|
||||||
feedbackApiClient: this.props.feedbackApiClient,
|
feedbackStore: this.props.feedbackStore,
|
||||||
onAfterFeedbackReceived: this.callStatusSwitcher("start")}
|
onAfterFeedbackReceived: this.callStatusSwitcher("start")}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
|
@ -887,14 +892,14 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
// XXX New types for flux style
|
// XXX New types for flux style
|
||||||
standaloneAppStore: React.PropTypes.instanceOf(
|
standaloneAppStore: React.PropTypes.instanceOf(
|
||||||
loop.store.StandaloneAppStore).isRequired,
|
loop.store.StandaloneAppStore).isRequired,
|
||||||
activeRoomStore: React.PropTypes.instanceOf(
|
activeRoomStore: React.PropTypes.instanceOf(
|
||||||
loop.store.ActiveRoomStore).isRequired,
|
loop.store.ActiveRoomStore).isRequired,
|
||||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -931,7 +936,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
helper: this.props.helper,
|
helper: this.props.helper,
|
||||||
notifications: this.props.notifications,
|
notifications: this.props.notifications,
|
||||||
sdk: this.props.sdk,
|
sdk: this.props.sdk,
|
||||||
feedbackApiClient: this.props.feedbackApiClient}
|
feedbackStore: this.props.feedbackStore}
|
||||||
)
|
)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -992,7 +997,14 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
sdk: OT
|
sdk: OT
|
||||||
});
|
});
|
||||||
|
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({
|
var standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
|
@ -1003,6 +1015,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
mozLoop: standaloneMozLoop,
|
mozLoop: standaloneMozLoop,
|
||||||
sdkDriver: sdkDriver
|
sdkDriver: sdkDriver
|
||||||
});
|
});
|
||||||
|
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: feedbackClient
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener("unload", function() {
|
window.addEventListener("unload", function() {
|
||||||
dispatcher.dispatch(new sharedActions.WindowUnload());
|
dispatcher.dispatch(new sharedActions.WindowUnload());
|
||||||
|
@ -1014,7 +1029,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
helper: helper,
|
helper: helper,
|
||||||
notifications: notifications,
|
notifications: notifications,
|
||||||
sdk: OT,
|
sdk: OT,
|
||||||
feedbackApiClient: feedbackApiClient,
|
feedbackStore: feedbackStore,
|
||||||
standaloneAppStore: standaloneAppStore,
|
standaloneAppStore: standaloneAppStore,
|
||||||
activeRoomStore: activeRoomStore,
|
activeRoomStore: activeRoomStore,
|
||||||
dispatcher: dispatcher}
|
dispatcher: dispatcher}
|
||||||
|
|
|
@ -260,6 +260,11 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
<div title={mozL10n.get("vendor_alttext",
|
<div title={mozL10n.get("vendor_alttext",
|
||||||
{vendorShortname: mozL10n.get("vendorShortname")})}
|
{vendorShortname: mozL10n.get("vendorShortname")})}
|
||||||
className="footer-logo"></div>
|
className="footer-logo"></div>
|
||||||
|
<div className="footer-external-links">
|
||||||
|
<a target="_blank" href={loop.config.guestSupportUrl}>
|
||||||
|
{mozL10n.get("support_link")}
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -538,7 +543,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore),
|
||||||
onAfterFeedbackReceived: React.PropTypes.func.isRequired
|
onAfterFeedbackReceived: React.PropTypes.func.isRequired
|
||||||
},
|
},
|
||||||
|
|
||||||
|
@ -549,7 +554,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
return (
|
return (
|
||||||
<div className="ended-conversation">
|
<div className="ended-conversation">
|
||||||
<sharedViews.FeedbackView
|
<sharedViews.FeedbackView
|
||||||
feedbackApiClient={this.props.feedbackApiClient}
|
feedbackStore={this.props.feedbackStore}
|
||||||
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived}
|
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived}
|
||||||
/>
|
/>
|
||||||
<sharedViews.ConversationView
|
<sharedViews.ConversationView
|
||||||
|
@ -611,7 +616,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -690,7 +695,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
<EndedConversationView
|
<EndedConversationView
|
||||||
sdk={this.props.sdk}
|
sdk={this.props.sdk}
|
||||||
conversation={this.props.conversation}
|
conversation={this.props.conversation}
|
||||||
feedbackApiClient={this.props.feedbackApiClient}
|
feedbackStore={this.props.feedbackStore}
|
||||||
onAfterFeedbackReceived={this.callStatusSwitcher("start")}
|
onAfterFeedbackReceived={this.callStatusSwitcher("start")}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
@ -887,14 +892,14 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
|
||||||
.isRequired,
|
.isRequired,
|
||||||
sdk: React.PropTypes.object.isRequired,
|
sdk: React.PropTypes.object.isRequired,
|
||||||
feedbackApiClient: React.PropTypes.object.isRequired,
|
|
||||||
|
|
||||||
// XXX New types for flux style
|
// XXX New types for flux style
|
||||||
standaloneAppStore: React.PropTypes.instanceOf(
|
standaloneAppStore: React.PropTypes.instanceOf(
|
||||||
loop.store.StandaloneAppStore).isRequired,
|
loop.store.StandaloneAppStore).isRequired,
|
||||||
activeRoomStore: React.PropTypes.instanceOf(
|
activeRoomStore: React.PropTypes.instanceOf(
|
||||||
loop.store.ActiveRoomStore).isRequired,
|
loop.store.ActiveRoomStore).isRequired,
|
||||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired
|
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||||
|
feedbackStore: React.PropTypes.instanceOf(loop.store.FeedbackStore)
|
||||||
},
|
},
|
||||||
|
|
||||||
getInitialState: function() {
|
getInitialState: function() {
|
||||||
|
@ -931,7 +936,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
helper={this.props.helper}
|
helper={this.props.helper}
|
||||||
notifications={this.props.notifications}
|
notifications={this.props.notifications}
|
||||||
sdk={this.props.sdk}
|
sdk={this.props.sdk}
|
||||||
feedbackApiClient={this.props.feedbackApiClient}
|
feedbackStore={this.props.feedbackStore}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -992,7 +997,14 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
sdk: OT
|
sdk: OT
|
||||||
});
|
});
|
||||||
|
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({
|
var standaloneAppStore = new loop.store.StandaloneAppStore({
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
|
@ -1003,6 +1015,9 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
mozLoop: standaloneMozLoop,
|
mozLoop: standaloneMozLoop,
|
||||||
sdkDriver: sdkDriver
|
sdkDriver: sdkDriver
|
||||||
});
|
});
|
||||||
|
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: feedbackClient
|
||||||
|
});
|
||||||
|
|
||||||
window.addEventListener("unload", function() {
|
window.addEventListener("unload", function() {
|
||||||
dispatcher.dispatch(new sharedActions.WindowUnload());
|
dispatcher.dispatch(new sharedActions.WindowUnload());
|
||||||
|
@ -1014,7 +1029,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
|
||||||
helper={helper}
|
helper={helper}
|
||||||
notifications={notifications}
|
notifications={notifications}
|
||||||
sdk={OT}
|
sdk={OT}
|
||||||
feedbackApiClient={feedbackApiClient}
|
feedbackStore={feedbackStore}
|
||||||
standaloneAppStore={standaloneAppStore}
|
standaloneAppStore={standaloneAppStore}
|
||||||
activeRoomStore={activeRoomStore}
|
activeRoomStore={activeRoomStore}
|
||||||
dispatcher={dispatcher}
|
dispatcher={dispatcher}
|
||||||
|
|
|
@ -124,4 +124,4 @@ standalone_title_with_status={{clientShortname}} — {{currentStatus}}
|
||||||
status_in_conversation=In conversation
|
status_in_conversation=In conversation
|
||||||
status_conversation_ended=Conversation ended
|
status_conversation_ended=Conversation ended
|
||||||
status_error=Something went wrong
|
status_error=Something went wrong
|
||||||
|
support_link=Get Help
|
||||||
|
|
|
@ -30,7 +30,9 @@ function getConfigFile(req, res) {
|
||||||
"loop.config.legalWebsiteUrl = '/legal/terms';",
|
"loop.config.legalWebsiteUrl = '/legal/terms';",
|
||||||
"loop.config.fxosApp = loop.config.fxosApp || {};",
|
"loop.config.fxosApp = loop.config.fxosApp || {};",
|
||||||
"loop.config.fxosApp.name = 'Loop';",
|
"loop.config.fxosApp.name = 'Loop';",
|
||||||
"loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';"
|
"loop.config.fxosApp.manifestUrl = 'http://fake-market.herokuapp.com/apps/packagedApp/manifest.webapp';",
|
||||||
|
"loop.config.roomsSupportUrl = 'https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc';",
|
||||||
|
"loop.config.guestSupportUrl = 'https://support.mozilla.org/kb/respond-firefox-hello-invitation-guest-mode';"
|
||||||
].join("\n"));
|
].join("\n"));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -445,13 +445,14 @@ describe("loop.conversationViews", function () {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("OutgoingConversationView", function() {
|
describe("OutgoingConversationView", function() {
|
||||||
var store;
|
var store, feedbackStore;
|
||||||
|
|
||||||
function mountTestComponent() {
|
function mountTestComponent() {
|
||||||
return TestUtils.renderIntoDocument(
|
return TestUtils.renderIntoDocument(
|
||||||
loop.conversationViews.OutgoingConversationView({
|
loop.conversationViews.OutgoingConversationView({
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
store: store
|
store: store,
|
||||||
|
feedbackStore: feedbackStore
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -461,6 +462,9 @@ describe("loop.conversationViews", function () {
|
||||||
client: {},
|
client: {},
|
||||||
sdkDriver: {}
|
sdkDriver: {}
|
||||||
});
|
});
|
||||||
|
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: {}
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
it("should render the CallFailedView when the call state is 'terminated'",
|
it("should render the CallFailedView when the call state is 'terminated'",
|
||||||
|
|
|
@ -233,7 +233,8 @@ describe("loop.conversation", function() {
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("IncomingConversationView", function() {
|
describe("IncomingConversationView", function() {
|
||||||
var conversationAppStore, conversation, client, icView, oldTitle;
|
var conversationAppStore, conversation, client, icView, oldTitle,
|
||||||
|
feedbackStore;
|
||||||
|
|
||||||
function mountTestComponent() {
|
function mountTestComponent() {
|
||||||
return TestUtils.renderIntoDocument(
|
return TestUtils.renderIntoDocument(
|
||||||
|
@ -241,7 +242,8 @@ describe("loop.conversation", function() {
|
||||||
client: client,
|
client: client,
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
sdk: {},
|
sdk: {},
|
||||||
conversationAppStore: conversationAppStore
|
conversationAppStore: conversationAppStore,
|
||||||
|
feedbackStore: feedbackStore
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -257,6 +259,9 @@ describe("loop.conversation", function() {
|
||||||
dispatcher: dispatcher,
|
dispatcher: dispatcher,
|
||||||
mozLoop: navigator.mozLoop
|
mozLoop: navigator.mozLoop
|
||||||
});
|
});
|
||||||
|
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: {}
|
||||||
|
});
|
||||||
sandbox.stub(conversation, "setOutgoingSessionData");
|
sandbox.stub(conversation, "setOutgoingSessionData");
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
@ -46,6 +46,8 @@
|
||||||
<script src="../../content/shared/js/store.js"></script>
|
<script src="../../content/shared/js/store.js"></script>
|
||||||
<script src="../../content/shared/js/roomStore.js"></script>
|
<script src="../../content/shared/js/roomStore.js"></script>
|
||||||
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
||||||
|
<script src="../../content/shared/js/feedbackStore.js"></script>
|
||||||
|
<script src="../../content/shared/js/feedbackViews.js"></script>
|
||||||
<script src="../../content/js/client.js"></script>
|
<script src="../../content/js/client.js"></script>
|
||||||
<script src="../../content/js/conversationAppStore.js"></script>
|
<script src="../../content/js/conversationAppStore.js"></script>
|
||||||
<script src="../../content/js/roomViews.js"></script>
|
<script src="../../content/js/roomViews.js"></script>
|
||||||
|
|
|
@ -356,6 +356,31 @@ describe("loop.panel", function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe("Help", function() {
|
||||||
|
var supportUrl = "https://example.com";
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
navigator.mozLoop.getLoopPref = function(pref) {
|
||||||
|
if (pref === "support_url")
|
||||||
|
return supportUrl;
|
||||||
|
return "unseen";
|
||||||
|
};
|
||||||
|
|
||||||
|
sandbox.stub(window, "open");
|
||||||
|
sandbox.stub(window, "close");
|
||||||
|
});
|
||||||
|
|
||||||
|
it("should open a tab to the support page", function() {
|
||||||
|
var view = TestUtils.renderIntoDocument(loop.panel.SettingsDropdown());
|
||||||
|
|
||||||
|
TestUtils.Simulate
|
||||||
|
.click(view.getDOMNode().querySelector(".icon-help"));
|
||||||
|
|
||||||
|
sinon.assert.calledOnce(window.open);
|
||||||
|
sinon.assert.calledWithExactly(window.open, supportUrl);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe("#render", function() {
|
describe("#render", function() {
|
||||||
it("should render a ToSView", function() {
|
it("should render a ToSView", function() {
|
||||||
var view = createTestPanelView();
|
var view = createTestPanelView();
|
||||||
|
|
|
@ -0,0 +1,108 @@
|
||||||
|
/* global chai, loop */
|
||||||
|
|
||||||
|
var expect = chai.expect;
|
||||||
|
var sharedActions = loop.shared.actions;
|
||||||
|
|
||||||
|
describe("loop.store.FeedbackStore", function () {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
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 store = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: feedbackClient
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(store.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));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -0,0 +1,209 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
/*global loop, sinon, React */
|
||||||
|
/* jshint newcap:false */
|
||||||
|
|
||||||
|
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;
|
||||||
|
|
||||||
|
describe("loop.shared.views.FeedbackView", function() {
|
||||||
|
"use strict";
|
||||||
|
|
||||||
|
var sandbox, comp, dispatcher, feedbackStore, fakeAudioXHR, fakeFeedbackClient;
|
||||||
|
|
||||||
|
beforeEach(function() {
|
||||||
|
sandbox = sinon.sandbox.create();
|
||||||
|
fakeAudioXHR = {
|
||||||
|
open: sinon.spy(),
|
||||||
|
send: function() {},
|
||||||
|
abort: function() {},
|
||||||
|
getResponseHeader: function(header) {
|
||||||
|
if (header === "Content-Type")
|
||||||
|
return "audio/ogg";
|
||||||
|
},
|
||||||
|
responseType: null,
|
||||||
|
response: new ArrayBuffer(10),
|
||||||
|
onload: null
|
||||||
|
};
|
||||||
|
dispatcher = new loop.Dispatcher();
|
||||||
|
fakeFeedbackClient = {send: sandbox.stub()};
|
||||||
|
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: fakeFeedbackClient
|
||||||
|
});
|
||||||
|
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
|
||||||
|
comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
|
||||||
|
feedbackStore: feedbackStore
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
afterEach(function() {
|
||||||
|
sandbox.restore();
|
||||||
|
});
|
||||||
|
|
||||||
|
// local test helpers
|
||||||
|
function clickHappyFace(comp) {
|
||||||
|
var happyFace = comp.getDOMNode().querySelector(".face-happy");
|
||||||
|
TestUtils.Simulate.click(happyFace);
|
||||||
|
}
|
||||||
|
|
||||||
|
function clickSadFace(comp) {
|
||||||
|
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
||||||
|
TestUtils.Simulate.click(sadFace);
|
||||||
|
}
|
||||||
|
|
||||||
|
function fillSadFeedbackForm(comp, category, text) {
|
||||||
|
TestUtils.Simulate.change(
|
||||||
|
comp.getDOMNode().querySelector("[value='" + category + "']"));
|
||||||
|
|
||||||
|
if (text) {
|
||||||
|
TestUtils.Simulate.change(
|
||||||
|
comp.getDOMNode().querySelector("[name='description']"), {
|
||||||
|
target: {value: "fake reason"}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function submitSadFeedbackForm(comp, category, text) {
|
||||||
|
TestUtils.Simulate.submit(comp.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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
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 empty the description field when a predefined category is " +
|
||||||
|
"chosen",
|
||||||
|
function() {
|
||||||
|
clickSadFace(comp);
|
||||||
|
|
||||||
|
fillSadFeedbackForm(comp, "confusing");
|
||||||
|
|
||||||
|
expect(comp.getDOMNode().querySelector(".feedback-description").value).eql("");
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
|
@ -47,6 +47,8 @@
|
||||||
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
||||||
<script src="../../content/shared/js/roomStore.js"></script>
|
<script src="../../content/shared/js/roomStore.js"></script>
|
||||||
<script src="../../content/shared/js/conversationStore.js"></script>
|
<script src="../../content/shared/js/conversationStore.js"></script>
|
||||||
|
<script src="../../content/shared/js/feedbackStore.js"></script>
|
||||||
|
<script src="../../content/shared/js/feedbackViews.js"></script>
|
||||||
|
|
||||||
<!-- Test scripts -->
|
<!-- Test scripts -->
|
||||||
<script src="models_test.js"></script>
|
<script src="models_test.js"></script>
|
||||||
|
@ -55,10 +57,12 @@
|
||||||
<script src="views_test.js"></script>
|
<script src="views_test.js"></script>
|
||||||
<script src="websocket_test.js"></script>
|
<script src="websocket_test.js"></script>
|
||||||
<script src="feedbackApiClient_test.js"></script>
|
<script src="feedbackApiClient_test.js"></script>
|
||||||
|
<script src="feedbackViews_test.js"></script>
|
||||||
<script src="validate_test.js"></script>
|
<script src="validate_test.js"></script>
|
||||||
<script src="dispatcher_test.js"></script>
|
<script src="dispatcher_test.js"></script>
|
||||||
<script src="activeRoomStore_test.js"></script>
|
<script src="activeRoomStore_test.js"></script>
|
||||||
<script src="conversationStore_test.js"></script>
|
<script src="conversationStore_test.js"></script>
|
||||||
|
<script src="feedbackStore_test.js"></script>
|
||||||
<script src="otSdkDriver_test.js"></script>
|
<script src="otSdkDriver_test.js"></script>
|
||||||
<script src="store_test.js"></script>
|
<script src="store_test.js"></script>
|
||||||
<script src="roomStore_test.js"></script>
|
<script src="roomStore_test.js"></script>
|
||||||
|
|
|
@ -526,177 +526,6 @@ describe("loop.shared.views", function() {
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe("FeedbackView", function() {
|
|
||||||
var comp, fakeFeedbackApiClient;
|
|
||||||
|
|
||||||
beforeEach(function() {
|
|
||||||
fakeFeedbackApiClient = {send: sandbox.stub()};
|
|
||||||
sandbox.stub(window, "XMLHttpRequest").returns(fakeAudioXHR);
|
|
||||||
comp = TestUtils.renderIntoDocument(sharedViews.FeedbackView({
|
|
||||||
feedbackApiClient: fakeFeedbackApiClient
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
|
|
||||||
// local test helpers
|
|
||||||
function clickHappyFace(comp) {
|
|
||||||
var happyFace = comp.getDOMNode().querySelector(".face-happy");
|
|
||||||
TestUtils.Simulate.click(happyFace);
|
|
||||||
}
|
|
||||||
|
|
||||||
function clickSadFace(comp) {
|
|
||||||
var sadFace = comp.getDOMNode().querySelector(".face-sad");
|
|
||||||
TestUtils.Simulate.click(sadFace);
|
|
||||||
}
|
|
||||||
|
|
||||||
function fillSadFeedbackForm(comp, category, text) {
|
|
||||||
TestUtils.Simulate.change(
|
|
||||||
comp.getDOMNode().querySelector("[value='" + category + "']"));
|
|
||||||
|
|
||||||
if (text) {
|
|
||||||
TestUtils.Simulate.change(
|
|
||||||
comp.getDOMNode().querySelector("[name='description']"), {
|
|
||||||
target: {value: "fake reason"}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function submitSadFeedbackForm(comp, category, text) {
|
|
||||||
TestUtils.Simulate.submit(comp.getDOMNode().querySelector("form"));
|
|
||||||
}
|
|
||||||
|
|
||||||
describe("Happy feedback", function() {
|
|
||||||
it("should send feedback data when clicking on the happy face",
|
|
||||||
function() {
|
|
||||||
clickHappyFace(comp);
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(fakeFeedbackApiClient.send);
|
|
||||||
sinon.assert.calledWith(fakeFeedbackApiClient.send, {happy: true});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should thank the user once happy feedback data is sent", function() {
|
|
||||||
fakeFeedbackApiClient.send = function(data, cb) {
|
|
||||||
cb();
|
|
||||||
};
|
|
||||||
|
|
||||||
clickHappyFace(comp);
|
|
||||||
|
|
||||||
expect(comp.getDOMNode()
|
|
||||||
.querySelectorAll(".feedback .thank-you").length).eql(1);
|
|
||||||
expect(comp.getDOMNode().querySelector("button.back")).to.be.a("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").length).eql(1);
|
|
||||||
});
|
|
||||||
|
|
||||||
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 empty the description field when a predefined category is " +
|
|
||||||
"chosen",
|
|
||||||
function() {
|
|
||||||
clickSadFace(comp);
|
|
||||||
|
|
||||||
fillSadFeedbackForm(comp, "confusing");
|
|
||||||
|
|
||||||
expect(comp.getDOMNode()
|
|
||||||
.querySelector(".feedback-description").value).eql("");
|
|
||||||
});
|
|
||||||
|
|
||||||
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 disable the form submit button once the form is submitted",
|
|
||||||
function() {
|
|
||||||
clickSadFace(comp);
|
|
||||||
fillSadFeedbackForm(comp, "confusing");
|
|
||||||
|
|
||||||
submitSadFeedbackForm(comp);
|
|
||||||
|
|
||||||
expect(comp.getDOMNode()
|
|
||||||
.querySelector("form button").disabled).eql(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should send feedback data when the form is submitted", function() {
|
|
||||||
clickSadFace(comp);
|
|
||||||
fillSadFeedbackForm(comp, "confusing");
|
|
||||||
|
|
||||||
submitSadFeedbackForm(comp);
|
|
||||||
|
|
||||||
sinon.assert.calledOnce(fakeFeedbackApiClient.send);
|
|
||||||
sinon.assert.calledWithMatch(fakeFeedbackApiClient.send, {
|
|
||||||
happy: false,
|
|
||||||
category: "confusing"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
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(fakeFeedbackApiClient.send);
|
|
||||||
sinon.assert.calledWith(fakeFeedbackApiClient.send, {
|
|
||||||
happy: false,
|
|
||||||
category: "other",
|
|
||||||
description: "fake reason"
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
it("should thank the user when feedback data has been sent", function() {
|
|
||||||
fakeFeedbackApiClient.send = function(data, cb) {
|
|
||||||
cb();
|
|
||||||
};
|
|
||||||
clickSadFace(comp);
|
|
||||||
fillSadFeedbackForm(comp, "confusing");
|
|
||||||
submitSadFeedbackForm(comp);
|
|
||||||
|
|
||||||
expect(comp.getDOMNode()
|
|
||||||
.querySelectorAll(".feedback .thank-you").length).eql(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe("NotificationListView", function() {
|
describe("NotificationListView", function() {
|
||||||
var coll, view, testNotif;
|
var coll, view, testNotif;
|
||||||
|
|
||||||
|
|
|
@ -42,6 +42,8 @@
|
||||||
<script src="../../content/shared/js/dispatcher.js"></script>
|
<script src="../../content/shared/js/dispatcher.js"></script>
|
||||||
<script src="../../content/shared/js/store.js"></script>
|
<script src="../../content/shared/js/store.js"></script>
|
||||||
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
<script src="../../content/shared/js/activeRoomStore.js"></script>
|
||||||
|
<script src="../../content/shared/js/feedbackStore.js"></script>
|
||||||
|
<script src="../../content/shared/js/feedbackViews.js"></script>
|
||||||
<script src="../../content/shared/js/otSdkDriver.js"></script>
|
<script src="../../content/shared/js/otSdkDriver.js"></script>
|
||||||
<script src="../../standalone/content/js/multiplexGum.js"></script>
|
<script src="../../standalone/content/js/multiplexGum.js"></script>
|
||||||
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
|
<script src="../../standalone/content/js/standaloneAppStore.js"></script>
|
||||||
|
|
|
@ -19,14 +19,20 @@ describe("loop.webapp", function() {
|
||||||
notifications,
|
notifications,
|
||||||
feedbackApiClient,
|
feedbackApiClient,
|
||||||
stubGetPermsAndCacheMedia,
|
stubGetPermsAndCacheMedia,
|
||||||
fakeAudioXHR;
|
fakeAudioXHR,
|
||||||
|
dispatcher,
|
||||||
|
feedbackStore;
|
||||||
|
|
||||||
beforeEach(function() {
|
beforeEach(function() {
|
||||||
sandbox = sinon.sandbox.create();
|
sandbox = sinon.sandbox.create();
|
||||||
|
dispatcher = new loop.Dispatcher();
|
||||||
notifications = new sharedModels.NotificationCollection();
|
notifications = new sharedModels.NotificationCollection();
|
||||||
feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", {
|
feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", {
|
||||||
product: "Loop"
|
product: "Loop"
|
||||||
});
|
});
|
||||||
|
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: {}
|
||||||
|
});
|
||||||
|
|
||||||
stubGetPermsAndCacheMedia = sandbox.stub(
|
stubGetPermsAndCacheMedia = sandbox.stub(
|
||||||
loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
|
loop.standaloneMedia._MultiplexGum.prototype, "getPermsAndCacheMedia");
|
||||||
|
@ -123,7 +129,7 @@ describe("loop.webapp", function() {
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
notifications: notifications,
|
notifications: notifications,
|
||||||
sdk: {},
|
sdk: {},
|
||||||
feedbackApiClient: feedbackApiClient
|
feedbackStore: feedbackStore
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -582,7 +588,7 @@ describe("loop.webapp", function() {
|
||||||
|
|
||||||
describe("WebappRootView", function() {
|
describe("WebappRootView", function() {
|
||||||
var helper, sdk, conversationModel, client, props, standaloneAppStore;
|
var helper, sdk, conversationModel, client, props, standaloneAppStore;
|
||||||
var dispatcher, activeRoomStore;
|
var activeRoomStore;
|
||||||
|
|
||||||
function mountTestComponent() {
|
function mountTestComponent() {
|
||||||
return TestUtils.renderIntoDocument(
|
return TestUtils.renderIntoDocument(
|
||||||
|
@ -609,7 +615,6 @@ describe("loop.webapp", function() {
|
||||||
client = new loop.StandaloneClient({
|
client = new loop.StandaloneClient({
|
||||||
baseServerUrl: "fakeUrl"
|
baseServerUrl: "fakeUrl"
|
||||||
});
|
});
|
||||||
dispatcher = new loop.Dispatcher();
|
|
||||||
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
|
||||||
mozLoop: {},
|
mozLoop: {},
|
||||||
sdkDriver: {}
|
sdkDriver: {}
|
||||||
|
@ -1039,7 +1044,7 @@ describe("loop.webapp", function() {
|
||||||
loop.webapp.EndedConversationView({
|
loop.webapp.EndedConversationView({
|
||||||
conversation: conversation,
|
conversation: conversation,
|
||||||
sdk: {},
|
sdk: {},
|
||||||
feedbackApiClient: feedbackApiClient,
|
feedbackStore: feedbackStore,
|
||||||
onAfterFeedbackReceived: function(){}
|
onAfterFeedbackReceived: function(){}
|
||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
|
@ -45,6 +45,8 @@
|
||||||
<script src="../content/shared/js/roomStore.js"></script>
|
<script src="../content/shared/js/roomStore.js"></script>
|
||||||
<script src="../content/shared/js/conversationStore.js"></script>
|
<script src="../content/shared/js/conversationStore.js"></script>
|
||||||
<script src="../content/shared/js/activeRoomStore.js"></script>
|
<script src="../content/shared/js/activeRoomStore.js"></script>
|
||||||
|
<script src="../content/shared/js/feedbackStore.js"></script>
|
||||||
|
<script src="../content/shared/js/feedbackViews.js"></script>
|
||||||
<script src="../content/js/roomViews.js"></script>
|
<script src="../content/js/roomViews.js"></script>
|
||||||
<script src="../content/js/conversationViews.js"></script>
|
<script src="../content/js/conversationViews.js"></script>
|
||||||
<script src="../content/js/client.js"></script>
|
<script src="../content/js/client.js"></script>
|
||||||
|
|
|
@ -39,8 +39,9 @@
|
||||||
var ConversationView = loop.shared.views.ConversationView;
|
var ConversationView = loop.shared.views.ConversationView;
|
||||||
var FeedbackView = loop.shared.views.FeedbackView;
|
var FeedbackView = loop.shared.views.FeedbackView;
|
||||||
|
|
||||||
// Room constants
|
// Store constants
|
||||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||||
|
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||||
|
|
||||||
// Local helpers
|
// Local helpers
|
||||||
function returnTrue() {
|
function returnTrue() {
|
||||||
|
@ -69,6 +70,9 @@
|
||||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||||
mozLoop: navigator.mozLoop
|
mozLoop: navigator.mozLoop
|
||||||
});
|
});
|
||||||
|
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: stageFeedbackApiClient
|
||||||
|
});
|
||||||
|
|
||||||
// Local mocks
|
// Local mocks
|
||||||
|
|
||||||
|
@ -460,13 +464,13 @@
|
||||||
React.DOM.a({href: "https://input.allizom.org/"}, "input.allizom.org"), "."
|
React.DOM.a({href: "https://input.allizom.org/"}, "input.allizom.org"), "."
|
||||||
),
|
),
|
||||||
Example({summary: "Default (useable demo)", dashed: "true", style: {width: "260px"}},
|
Example({summary: "Default (useable demo)", dashed: "true", style: {width: "260px"}},
|
||||||
FeedbackView({feedbackApiClient: stageFeedbackApiClient})
|
FeedbackView({feedbackStore: feedbackStore})
|
||||||
),
|
),
|
||||||
Example({summary: "Detailed form", dashed: "true", style: {width: "260px"}},
|
Example({summary: "Detailed form", dashed: "true", style: {width: "260px"}},
|
||||||
FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "form"})
|
FeedbackView({feedbackStore: feedbackStore, feedbackState: FEEDBACK_STATES.DETAILS})
|
||||||
),
|
),
|
||||||
Example({summary: "Thank you!", dashed: "true", style: {width: "260px"}},
|
Example({summary: "Thank you!", dashed: "true", style: {width: "260px"}},
|
||||||
FeedbackView({feedbackApiClient: stageFeedbackApiClient, step: "finished"})
|
FeedbackView({feedbackStore: feedbackStore, feedbackState: FEEDBACK_STATES.SENT})
|
||||||
)
|
)
|
||||||
),
|
),
|
||||||
|
|
||||||
|
@ -486,7 +490,7 @@
|
||||||
video: {enabled: true},
|
video: {enabled: true},
|
||||||
audio: {enabled: true},
|
audio: {enabled: true},
|
||||||
conversation: mockConversationModel,
|
conversation: mockConversationModel,
|
||||||
feedbackApiClient: stageFeedbackApiClient,
|
feedbackStore: feedbackStore,
|
||||||
onAfterFeedbackReceived: noop})
|
onAfterFeedbackReceived: noop})
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
|
@ -39,8 +39,9 @@
|
||||||
var ConversationView = loop.shared.views.ConversationView;
|
var ConversationView = loop.shared.views.ConversationView;
|
||||||
var FeedbackView = loop.shared.views.FeedbackView;
|
var FeedbackView = loop.shared.views.FeedbackView;
|
||||||
|
|
||||||
// Room constants
|
// Store constants
|
||||||
var ROOM_STATES = loop.store.ROOM_STATES;
|
var ROOM_STATES = loop.store.ROOM_STATES;
|
||||||
|
var FEEDBACK_STATES = loop.store.FEEDBACK_STATES;
|
||||||
|
|
||||||
// Local helpers
|
// Local helpers
|
||||||
function returnTrue() {
|
function returnTrue() {
|
||||||
|
@ -69,6 +70,9 @@
|
||||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||||
mozLoop: navigator.mozLoop
|
mozLoop: navigator.mozLoop
|
||||||
});
|
});
|
||||||
|
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
|
||||||
|
feedbackClient: stageFeedbackApiClient
|
||||||
|
});
|
||||||
|
|
||||||
// Local mocks
|
// Local mocks
|
||||||
|
|
||||||
|
@ -460,13 +464,13 @@
|
||||||
<a href="https://input.allizom.org/">input.allizom.org</a>.
|
<a href="https://input.allizom.org/">input.allizom.org</a>.
|
||||||
</p>
|
</p>
|
||||||
<Example summary="Default (useable demo)" dashed="true" style={{width: "260px"}}>
|
<Example summary="Default (useable demo)" dashed="true" style={{width: "260px"}}>
|
||||||
<FeedbackView feedbackApiClient={stageFeedbackApiClient} />
|
<FeedbackView feedbackStore={feedbackStore} />
|
||||||
</Example>
|
</Example>
|
||||||
<Example summary="Detailed form" dashed="true" style={{width: "260px"}}>
|
<Example summary="Detailed form" dashed="true" style={{width: "260px"}}>
|
||||||
<FeedbackView feedbackApiClient={stageFeedbackApiClient} step="form" />
|
<FeedbackView feedbackStore={feedbackStore} feedbackState={FEEDBACK_STATES.DETAILS} />
|
||||||
</Example>
|
</Example>
|
||||||
<Example summary="Thank you!" dashed="true" style={{width: "260px"}}>
|
<Example summary="Thank you!" dashed="true" style={{width: "260px"}}>
|
||||||
<FeedbackView feedbackApiClient={stageFeedbackApiClient} step="finished" />
|
<FeedbackView feedbackStore={feedbackStore} feedbackState={FEEDBACK_STATES.SENT} />
|
||||||
</Example>
|
</Example>
|
||||||
</Section>
|
</Section>
|
||||||
|
|
||||||
|
@ -486,7 +490,7 @@
|
||||||
video={{enabled: true}}
|
video={{enabled: true}}
|
||||||
audio={{enabled: true}}
|
audio={{enabled: true}}
|
||||||
conversation={mockConversationModel}
|
conversation={mockConversationModel}
|
||||||
feedbackApiClient={stageFeedbackApiClient}
|
feedbackStore={feedbackStore}
|
||||||
onAfterFeedbackReceived={noop} />
|
onAfterFeedbackReceived={noop} />
|
||||||
</div>
|
</div>
|
||||||
</Example>
|
</Example>
|
||||||
|
|
|
@ -16,14 +16,15 @@ const { indexedDB } = require("sdk/indexed-db");
|
||||||
|
|
||||||
const IDB = {
|
const IDB = {
|
||||||
_db: null,
|
_db: null,
|
||||||
|
databaseName: "AppProjects",
|
||||||
|
|
||||||
open: function () {
|
open: function () {
|
||||||
let deferred = promise.defer();
|
let deferred = promise.defer();
|
||||||
|
|
||||||
let request = indexedDB.open("AppProjects", 5);
|
let request = indexedDB.open(IDB.databaseName, 5);
|
||||||
request.onerror = function(event) {
|
request.onerror = function(event) {
|
||||||
deferred.reject("Unable to open AppProjects indexedDB. " +
|
deferred.reject("Unable to open AppProjects indexedDB: " +
|
||||||
"Error code: " + event.target.errorCode);
|
this.error.name + " - " + this.error.message );
|
||||||
};
|
};
|
||||||
request.onupgradeneeded = function(event) {
|
request.onupgradeneeded = function(event) {
|
||||||
let db = event.target.result;
|
let db = event.target.result;
|
||||||
|
@ -147,11 +148,10 @@ const store = new ObservableObject({ projects:[] });
|
||||||
|
|
||||||
let loadDeferred = promise.defer();
|
let loadDeferred = promise.defer();
|
||||||
|
|
||||||
IDB.open().then(function (projects) {
|
loadDeferred.resolve(IDB.open().then(function (projects) {
|
||||||
store.object.projects = projects;
|
store.object.projects = projects;
|
||||||
AppProjects.emit("ready", store.object.projects);
|
AppProjects.emit("ready", store.object.projects);
|
||||||
loadDeferred.resolve();
|
}));
|
||||||
});
|
|
||||||
|
|
||||||
const AppProjects = {
|
const AppProjects = {
|
||||||
load: function() {
|
load: function() {
|
||||||
|
|
|
@ -72,6 +72,9 @@ let UI = {
|
||||||
|
|
||||||
AppProjects.load().then(() => {
|
AppProjects.load().then(() => {
|
||||||
this.autoSelectProject();
|
this.autoSelectProject();
|
||||||
|
}, e => {
|
||||||
|
console.error(e);
|
||||||
|
this.reportError("error_appProjectsLoadFailed");
|
||||||
});
|
});
|
||||||
|
|
||||||
// Auto install the ADB Addon Helper and Tools Adapters. Only once.
|
// Auto install the ADB Addon Helper and Tools Adapters. Only once.
|
||||||
|
@ -256,7 +259,7 @@ let UI = {
|
||||||
this._busyTimeout = setTimeout(() => {
|
this._busyTimeout = setTimeout(() => {
|
||||||
this.unbusy();
|
this.unbusy();
|
||||||
UI.reportError("error_operationTimeout", this._busyOperationDescription);
|
UI.reportError("error_operationTimeout", this._busyOperationDescription);
|
||||||
}, 6000);
|
}, Services.prefs.getIntPref("devtools.webide.busyTimeout"));
|
||||||
},
|
},
|
||||||
|
|
||||||
cancelBusyTimeout: function() {
|
cancelBusyTimeout: function() {
|
||||||
|
|
|
@ -32,3 +32,4 @@ pref("devtools.webide.widget.enabled", false);
|
||||||
pref("devtools.webide.widget.inNavbarByDefault", false);
|
pref("devtools.webide.widget.inNavbarByDefault", false);
|
||||||
#endif
|
#endif
|
||||||
pref("devtools.webide.zoom", "1");
|
pref("devtools.webide.zoom", "1");
|
||||||
|
pref("devtools.webide.busyTimeout", 10000);
|
||||||
|
|
|
@ -20,7 +20,7 @@ importHostedApp_title=Open Hosted App
|
||||||
importHostedApp_header=Enter Manifest URL
|
importHostedApp_header=Enter Manifest URL
|
||||||
|
|
||||||
notification_showTroubleShooting_label=Troubleshooting
|
notification_showTroubleShooting_label=Troubleshooting
|
||||||
notification_showTroubleShooting_accesskey=t
|
notification_showTroubleShooting_accesskey=T
|
||||||
|
|
||||||
# LOCALIZATION NOTE (project_tab_loading): This is shown as a temporary tab
|
# LOCALIZATION NOTE (project_tab_loading): This is shown as a temporary tab
|
||||||
# title for browser tab projects when the tab is still loading.
|
# title for browser tab projects when the tab is still loading.
|
||||||
|
@ -42,6 +42,8 @@ error_cantConnectToApp=Can't connect to app: %1$S
|
||||||
# Variable: error message (in english)
|
# Variable: error message (in english)
|
||||||
error_cantFetchAddonsJSON=Can't fetch the add-on list: %S
|
error_cantFetchAddonsJSON=Can't fetch the add-on list: %S
|
||||||
|
|
||||||
|
error_appProjectsLoadFailed=Unable to load project list. This can occur if you've used this profile with a newer version of Firefox.
|
||||||
|
|
||||||
addons_stable=stable
|
addons_stable=stable
|
||||||
addons_unstable=unstable
|
addons_unstable=unstable
|
||||||
# LOCALIZATION NOTE (addons_simulator_label): This label is shown as the name of
|
# LOCALIZATION NOTE (addons_simulator_label): This label is shown as the name of
|
||||||
|
|
|
@ -7,6 +7,7 @@ package org.mozilla.gecko;
|
||||||
|
|
||||||
import java.io.File;
|
import java.io.File;
|
||||||
import java.io.FileNotFoundException;
|
import java.io.FileNotFoundException;
|
||||||
|
import java.lang.Override;
|
||||||
import java.lang.reflect.Method;
|
import java.lang.reflect.Method;
|
||||||
import java.net.URLEncoder;
|
import java.net.URLEncoder;
|
||||||
import java.util.EnumSet;
|
import java.util.EnumSet;
|
||||||
|
@ -24,6 +25,7 @@ import org.mozilla.gecko.DynamicToolbar.VisibilityTransition;
|
||||||
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
|
import org.mozilla.gecko.GeckoProfileDirectories.NoMozillaDirectoryException;
|
||||||
import org.mozilla.gecko.Tabs.TabEvents;
|
import org.mozilla.gecko.Tabs.TabEvents;
|
||||||
import org.mozilla.gecko.animation.PropertyAnimator;
|
import org.mozilla.gecko.animation.PropertyAnimator;
|
||||||
|
import org.mozilla.gecko.animation.TransitionsTracker;
|
||||||
import org.mozilla.gecko.animation.ViewHelper;
|
import org.mozilla.gecko.animation.ViewHelper;
|
||||||
import org.mozilla.gecko.db.BrowserContract.Combined;
|
import org.mozilla.gecko.db.BrowserContract.Combined;
|
||||||
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
|
import org.mozilla.gecko.db.BrowserContract.SearchHistory;
|
||||||
|
@ -1971,6 +1973,8 @@ public class BrowserApp extends GeckoApp
|
||||||
final PropertyAnimator animator = new PropertyAnimator(250);
|
final PropertyAnimator animator = new PropertyAnimator(250);
|
||||||
animator.setUseHardwareLayer(false);
|
animator.setUseHardwareLayer(false);
|
||||||
|
|
||||||
|
TransitionsTracker.track(animator);
|
||||||
|
|
||||||
mBrowserToolbar.startEditing(url, animator);
|
mBrowserToolbar.startEditing(url, animator);
|
||||||
|
|
||||||
final String panelId = selectedTab.getMostRecentHomePanel();
|
final String panelId = selectedTab.getMostRecentHomePanel();
|
||||||
|
|
|
@ -0,0 +1,119 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
package org.mozilla.gecko.animation;
|
||||||
|
|
||||||
|
import com.nineoldandroids.animation.Animator;
|
||||||
|
|
||||||
|
import java.util.ArrayList;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.animation.PropertyAnimator;
|
||||||
|
import org.mozilla.gecko.animation.PropertyAnimator.PropertyAnimationListener;
|
||||||
|
import org.mozilla.gecko.util.ThreadUtils;
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* {@link TransitionsTracker} provides a simple API to avoid running layout code
|
||||||
|
* during UI transitions. You should use it whenever you need to time-shift code
|
||||||
|
* that will likely trigger a layout traversal during an animation.
|
||||||
|
*/
|
||||||
|
public class TransitionsTracker {
|
||||||
|
private static final ArrayList<Runnable> pendingActions = new ArrayList<>();
|
||||||
|
private static int transitionCount;
|
||||||
|
|
||||||
|
private static final PropertyAnimationListener propertyAnimatorListener =
|
||||||
|
new PropertyAnimationListener() {
|
||||||
|
@Override
|
||||||
|
public void onPropertyAnimationStart() {
|
||||||
|
pushTransition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onPropertyAnimationEnd() {
|
||||||
|
popTransition();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static final Animator.AnimatorListener animatorListener =
|
||||||
|
new Animator.AnimatorListener() {
|
||||||
|
@Override
|
||||||
|
public void onAnimationStart(Animator animation) {
|
||||||
|
pushTransition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationEnd(Animator animation) {
|
||||||
|
popTransition();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationCancel(Animator animation) {
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onAnimationRepeat(Animator animation) {
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
private static void runPendingActions() {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
|
||||||
|
final int size = pendingActions.size();
|
||||||
|
for (int i = 0; i < size; i++) {
|
||||||
|
pendingActions.get(i).run();
|
||||||
|
}
|
||||||
|
|
||||||
|
pendingActions.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void pushTransition() {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
transitionCount++;
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void popTransition() {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
transitionCount--;
|
||||||
|
|
||||||
|
if (transitionCount < 0) {
|
||||||
|
throw new IllegalStateException("Invalid transition stack update");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (transitionCount == 0) {
|
||||||
|
runPendingActions();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean areTransitionsRunning() {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
return (transitionCount > 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void track(PropertyAnimator animator) {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
animator.addPropertyAnimationListener(propertyAnimatorListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void track(Animator animator) {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
animator.addListener(animatorListener);
|
||||||
|
}
|
||||||
|
|
||||||
|
public static boolean cancelPendingAction(Runnable action) {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
return pendingActions.removeAll(Collections.singleton(action));
|
||||||
|
}
|
||||||
|
|
||||||
|
public static void runAfterTransitions(Runnable action) {
|
||||||
|
ThreadUtils.assertOnUiThread();
|
||||||
|
|
||||||
|
if (transitionCount == 0) {
|
||||||
|
action.run();
|
||||||
|
} else {
|
||||||
|
pendingActions.add(action);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -216,7 +216,7 @@ public class BookmarksPanel extends HomeFragment {
|
||||||
/**
|
/**
|
||||||
* Loader callbacks for the LoaderManager of this fragment.
|
* Loader callbacks for the LoaderManager of this fragment.
|
||||||
*/
|
*/
|
||||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
if (args == null) {
|
if (args == null) {
|
||||||
|
@ -229,7 +229,7 @@ public class BookmarksPanel extends HomeFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
|
||||||
BookmarksLoader bl = (BookmarksLoader) loader;
|
BookmarksLoader bl = (BookmarksLoader) loader;
|
||||||
mListAdapter.swapCursor(c, bl.getFolderInfo(), bl.getRefreshType());
|
mListAdapter.swapCursor(c, bl.getFolderInfo(), bl.getRefreshType());
|
||||||
updateUiFromCursor(c);
|
updateUiFromCursor(c);
|
||||||
|
@ -237,6 +237,8 @@ public class BookmarksPanel extends HomeFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
|
|
||||||
if (mList != null) {
|
if (mList != null) {
|
||||||
mListAdapter.swapCursor(null);
|
mListAdapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -262,12 +262,6 @@ public class DynamicPanel extends HomeFragment {
|
||||||
public void requestDataset(DatasetRequest request) {
|
public void requestDataset(DatasetRequest request) {
|
||||||
Log.d(LOGTAG, "Requesting request: " + request);
|
Log.d(LOGTAG, "Requesting request: " + request);
|
||||||
|
|
||||||
// Ignore dataset requests while the fragment is not
|
|
||||||
// allowed to load its content.
|
|
||||||
if (!getCanLoadHint()) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
final Bundle bundle = new Bundle();
|
final Bundle bundle = new Bundle();
|
||||||
bundle.putParcelable(DATASET_REQUEST, request);
|
bundle.putParcelable(DATASET_REQUEST, request);
|
||||||
|
|
||||||
|
@ -352,7 +346,7 @@ public class DynamicPanel extends HomeFragment {
|
||||||
/**
|
/**
|
||||||
* LoaderCallbacks implementation that interacts with the LoaderManager.
|
* LoaderCallbacks implementation that interacts with the LoaderManager.
|
||||||
*/
|
*/
|
||||||
private class PanelLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class PanelLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST);
|
final DatasetRequest request = (DatasetRequest) args.getParcelable(DATASET_REQUEST);
|
||||||
|
@ -362,7 +356,7 @@ public class DynamicPanel extends HomeFragment {
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor cursor) {
|
public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor cursor) {
|
||||||
final DatasetRequest request = getRequestFromLoader(loader);
|
final DatasetRequest request = getRequestFromLoader(loader);
|
||||||
Log.d(LOGTAG, "Finished loader for request: " + request);
|
Log.d(LOGTAG, "Finished loader for request: " + request);
|
||||||
|
|
||||||
|
@ -373,6 +367,8 @@ public class DynamicPanel extends HomeFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
|
|
||||||
final DatasetRequest request = getRequestFromLoader(loader);
|
final DatasetRequest request = getRequestFromLoader(loader);
|
||||||
Log.d(LOGTAG, "Resetting loader for request: " + request);
|
Log.d(LOGTAG, "Resetting loader for request: " + request);
|
||||||
|
|
||||||
|
|
|
@ -471,20 +471,21 @@ public class HistoryPanel extends HomeFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
return new HistoryCursorLoader(getActivity());
|
return new HistoryCursorLoader(getActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
|
||||||
mAdapter.swapCursor(c);
|
mAdapter.swapCursor(c);
|
||||||
updateUiFromCursor(c);
|
updateUiFromCursor(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
mAdapter.swapCursor(null);
|
mAdapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -223,7 +223,7 @@ public class HomePager extends ViewPager {
|
||||||
|
|
||||||
final HomeAdapter adapter = new HomeAdapter(mContext, fm);
|
final HomeAdapter adapter = new HomeAdapter(mContext, fm);
|
||||||
adapter.setOnAddPanelListener(mAddPanelListener);
|
adapter.setOnAddPanelListener(mAddPanelListener);
|
||||||
adapter.setCanLoadHint(!shouldAnimate);
|
adapter.setCanLoadHint(true);
|
||||||
setAdapter(adapter);
|
setAdapter(adapter);
|
||||||
|
|
||||||
// Don't show the tabs strip until we have the
|
// Don't show the tabs strip until we have the
|
||||||
|
@ -243,7 +243,6 @@ public class HomePager extends ViewPager {
|
||||||
@Override
|
@Override
|
||||||
public void onPropertyAnimationEnd() {
|
public void onPropertyAnimationEnd() {
|
||||||
setLayerType(View.LAYER_TYPE_NONE, null);
|
setLayerType(View.LAYER_TYPE_NONE, null);
|
||||||
adapter.setCanLoadHint(true);
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@ -373,9 +372,7 @@ public class HomePager extends ViewPager {
|
||||||
final HomeAdapter adapter = (HomeAdapter) getAdapter();
|
final HomeAdapter adapter = (HomeAdapter) getAdapter();
|
||||||
|
|
||||||
// Disable any fragment loading until we have the initial
|
// Disable any fragment loading until we have the initial
|
||||||
// panel selection done. Store previous value to restore
|
// panel selection done.
|
||||||
// it if necessary once the UI is fully updated.
|
|
||||||
final boolean canLoadHint = adapter.getCanLoadHint();
|
|
||||||
adapter.setCanLoadHint(false);
|
adapter.setCanLoadHint(false);
|
||||||
|
|
||||||
// Destroy any existing panels currently loaded
|
// Destroy any existing panels currently loaded
|
||||||
|
@ -436,9 +433,6 @@ public class HomePager extends ViewPager {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the load hint was originally true, this means the pager
|
|
||||||
// is not animating and it's fine to restore the load hint back.
|
|
||||||
if (canLoadHint) {
|
|
||||||
// The selection is updated asynchronously so we need to post to
|
// The selection is updated asynchronously so we need to post to
|
||||||
// UI thread to give the pager time to commit the new page selection
|
// UI thread to give the pager time to commit the new page selection
|
||||||
// internally and load the right initial panel.
|
// internally and load the right initial panel.
|
||||||
|
@ -449,7 +443,6 @@ public class HomePager extends ViewPager {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
|
||||||
public void setOnPanelChangeListener(OnPanelChangeListener listener) {
|
public void setOnPanelChangeListener(OnPanelChangeListener listener) {
|
||||||
mPanelChangedListener = listener;
|
mPanelChangedListener = listener;
|
||||||
|
|
|
@ -8,6 +8,7 @@ package org.mozilla.gecko.home;
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
import org.mozilla.gecko.animation.BounceAnimator;
|
import org.mozilla.gecko.animation.BounceAnimator;
|
||||||
import org.mozilla.gecko.animation.BounceAnimator.Attributes;
|
import org.mozilla.gecko.animation.BounceAnimator.Attributes;
|
||||||
|
import org.mozilla.gecko.animation.TransitionsTracker;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
import android.content.res.Resources;
|
import android.content.res.Resources;
|
||||||
|
@ -120,6 +121,8 @@ class HomePagerTabStrip extends PagerTabStrip {
|
||||||
nextBounceAnimator.queue(new Attributes(0, BOUNCE4_MS));
|
nextBounceAnimator.queue(new Attributes(0, BOUNCE4_MS));
|
||||||
nextBounceAnimator.setStartDelay(ANIMATION_DELAY_MS);
|
nextBounceAnimator.setStartDelay(ANIMATION_DELAY_MS);
|
||||||
|
|
||||||
|
TransitionsTracker.track(nextBounceAnimator);
|
||||||
|
|
||||||
// Start animations.
|
// Start animations.
|
||||||
alphaAnimatorSet.start();
|
alphaAnimatorSet.start();
|
||||||
prevBounceAnimator.start();
|
prevBounceAnimator.start();
|
||||||
|
|
|
@ -197,20 +197,21 @@ public class ReadingListPanel extends HomeFragment {
|
||||||
/**
|
/**
|
||||||
* LoaderCallbacks implementation that interacts with the LoaderManager.
|
* LoaderCallbacks implementation that interacts with the LoaderManager.
|
||||||
*/
|
*/
|
||||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
return new ReadingListLoader(getActivity());
|
return new ReadingListLoader(getActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
|
||||||
mAdapter.swapCursor(c);
|
mAdapter.swapCursor(c);
|
||||||
updateUiFromCursor(c);
|
updateUiFromCursor(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
mAdapter.swapCursor(null);
|
mAdapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -404,20 +404,21 @@ public class RecentTabsPanel extends HomeFragment
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
return new RecentTabsCursorLoader(getActivity(), mClosedTabs);
|
return new RecentTabsCursorLoader(getActivity(), mClosedTabs);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
|
||||||
mAdapter.swapCursor(c);
|
mAdapter.swapCursor(c);
|
||||||
updateUiFromCursor(c);
|
updateUiFromCursor(c);
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
mAdapter.swapCursor(null);
|
mAdapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -392,14 +392,14 @@ public class RemoteTabsExpandableListFragment extends HomeFragment implements Re
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
return new RemoteTabsCursorLoader(getActivity());
|
return new RemoteTabsCursorLoader(getActivity());
|
||||||
}
|
}
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
public void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
|
||||||
final List<RemoteClient> clients = TabsAccessor.getClientsFromCursor(c);
|
final List<RemoteClient> clients = TabsAccessor.getClientsFromCursor(c);
|
||||||
|
|
||||||
// Filter the hidden clients out of the clients list. The clients
|
// Filter the hidden clients out of the clients list. The clients
|
||||||
|
@ -421,6 +421,7 @@ public class RemoteTabsExpandableListFragment extends HomeFragment implements Re
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
mAdapter.replaceClients(null);
|
mAdapter.replaceClients(null);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -689,7 +689,7 @@ public class TopSitesPanel extends HomeFragment {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private class CursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
private class CursorLoaderCallbacks extends TransitionAwareCursorLoaderCallbacks {
|
||||||
@Override
|
@Override
|
||||||
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
public Loader<Cursor> onCreateLoader(int id, Bundle args) {
|
||||||
trace("Creating TopSitesLoader: " + id);
|
trace("Creating TopSitesLoader: " + id);
|
||||||
|
@ -707,7 +707,7 @@ public class TopSitesPanel extends HomeFragment {
|
||||||
* Why that is... dunno.
|
* Why that is... dunno.
|
||||||
*/
|
*/
|
||||||
@Override
|
@Override
|
||||||
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
protected void onLoadFinishedAfterTransitions(Loader<Cursor> loader, Cursor c) {
|
||||||
debug("onLoadFinished: " + c.getCount() + " rows.");
|
debug("onLoadFinished: " + c.getCount() + " rows.");
|
||||||
|
|
||||||
mListAdapter.swapCursor(c);
|
mListAdapter.swapCursor(c);
|
||||||
|
@ -752,6 +752,8 @@ public class TopSitesPanel extends HomeFragment {
|
||||||
|
|
||||||
@Override
|
@Override
|
||||||
public void onLoaderReset(Loader<Cursor> loader) {
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
super.onLoaderReset(loader);
|
||||||
|
|
||||||
if (mListAdapter != null) {
|
if (mListAdapter != null) {
|
||||||
mListAdapter.swapCursor(null);
|
mListAdapter.swapCursor(null);
|
||||||
}
|
}
|
||||||
|
|
|
@ -0,0 +1,57 @@
|
||||||
|
/* 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/. */
|
||||||
|
|
||||||
|
package org.mozilla.gecko.home;
|
||||||
|
|
||||||
|
import android.database.Cursor;
|
||||||
|
import android.support.v4.app.LoaderManager.LoaderCallbacks;
|
||||||
|
import android.support.v4.content.Loader;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.animation.TransitionsTracker;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A {@link LoaderCallbacks} implementation that avoids running its
|
||||||
|
* {@link #onLoadFinished(Loader, Cursor)} method during animations as it's
|
||||||
|
* likely to trigger a layout traversal as a result of a cursor swap in the
|
||||||
|
* target adapter.
|
||||||
|
*/
|
||||||
|
public abstract class TransitionAwareCursorLoaderCallbacks implements LoaderCallbacks<Cursor> {
|
||||||
|
private OnLoadFinishedRunnable onLoadFinishedRunnable;
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoadFinished(Loader<Cursor> loader, Cursor c) {
|
||||||
|
if (onLoadFinishedRunnable != null) {
|
||||||
|
TransitionsTracker.cancelPendingAction(onLoadFinishedRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
onLoadFinishedRunnable = new OnLoadFinishedRunnable(loader, c);
|
||||||
|
TransitionsTracker.runAfterTransitions(onLoadFinishedRunnable);
|
||||||
|
}
|
||||||
|
|
||||||
|
protected abstract void onLoadFinishedAfterTransitions(Loader<Cursor> loade, Cursor c);
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void onLoaderReset(Loader<Cursor> loader) {
|
||||||
|
if (onLoadFinishedRunnable != null) {
|
||||||
|
TransitionsTracker.cancelPendingAction(onLoadFinishedRunnable);
|
||||||
|
onLoadFinishedRunnable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private class OnLoadFinishedRunnable implements Runnable {
|
||||||
|
private final Loader<Cursor> loader;
|
||||||
|
private final Cursor cursor;
|
||||||
|
|
||||||
|
public OnLoadFinishedRunnable(Loader<Cursor> loader, Cursor cursor) {
|
||||||
|
this.loader = loader;
|
||||||
|
this.cursor = cursor;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public void run() {
|
||||||
|
onLoadFinishedAfterTransitions(loader, cursor);
|
||||||
|
onLoadFinishedRunnable = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -124,6 +124,7 @@ gbjar.sources += [
|
||||||
'animation/HeightChangeAnimation.java',
|
'animation/HeightChangeAnimation.java',
|
||||||
'animation/PropertyAnimator.java',
|
'animation/PropertyAnimator.java',
|
||||||
'animation/Rotate3DAnimation.java',
|
'animation/Rotate3DAnimation.java',
|
||||||
|
'animation/TransitionsTracker.java',
|
||||||
'animation/ViewHelper.java',
|
'animation/ViewHelper.java',
|
||||||
'ANRReporter.java',
|
'ANRReporter.java',
|
||||||
'AppNotificationClient.java',
|
'AppNotificationClient.java',
|
||||||
|
@ -310,6 +311,7 @@ gbjar.sources += [
|
||||||
'home/TopSitesGridView.java',
|
'home/TopSitesGridView.java',
|
||||||
'home/TopSitesPanel.java',
|
'home/TopSitesPanel.java',
|
||||||
'home/TopSitesThumbnailView.java',
|
'home/TopSitesThumbnailView.java',
|
||||||
|
'home/TransitionAwareCursorLoaderCallbacks.java',
|
||||||
'home/TwoLinePageRow.java',
|
'home/TwoLinePageRow.java',
|
||||||
'InputMethods.java',
|
'InputMethods.java',
|
||||||
'IntentHelper.java',
|
'IntentHelper.java',
|
||||||
|
|
|
@ -12,6 +12,8 @@
|
||||||
android:layout_weight="1"
|
android:layout_weight="1"
|
||||||
android:paddingTop="4dp"/>
|
android:paddingTop="4dp"/>
|
||||||
|
|
||||||
|
<!-- The right margin creates a "dead area" on the right side of the button
|
||||||
|
which we compensate for with a touch delegate. See TabStrip -->
|
||||||
<ImageButton
|
<ImageButton
|
||||||
android:id="@+id/add_tab"
|
android:id="@+id/add_tab"
|
||||||
style="@style/UrlBar.ImageButton"
|
style="@style/UrlBar.ImageButton"
|
||||||
|
|
|
@ -6,9 +6,12 @@
|
||||||
package org.mozilla.gecko.tabs;
|
package org.mozilla.gecko.tabs;
|
||||||
|
|
||||||
import android.content.Context;
|
import android.content.Context;
|
||||||
|
import android.graphics.Rect;
|
||||||
import android.util.AttributeSet;
|
import android.util.AttributeSet;
|
||||||
import android.view.LayoutInflater;
|
import android.view.LayoutInflater;
|
||||||
|
import android.view.TouchDelegate;
|
||||||
import android.view.View;
|
import android.view.View;
|
||||||
|
import android.view.ViewTreeObserver;
|
||||||
import android.widget.ImageButton;
|
import android.widget.ImageButton;
|
||||||
|
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
|
@ -51,6 +54,25 @@ public class TabStrip extends ThemedLinearLayout {
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||||
|
@Override
|
||||||
|
public boolean onPreDraw() {
|
||||||
|
getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
|
|
||||||
|
final Rect r = new Rect();
|
||||||
|
r.left = addTabButton.getRight();
|
||||||
|
r.right = getWidth();
|
||||||
|
r.top = 0;
|
||||||
|
r.bottom = getHeight();
|
||||||
|
|
||||||
|
// Redirect touch events between the 'new tab' button and the edge
|
||||||
|
// of the screen to the 'new tab' button.
|
||||||
|
setTouchDelegate(new TouchDelegate(r, addTabButton));
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
tabsListener = new TabsListener();
|
tabsListener = new TabsListener();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -25,6 +25,7 @@ import com.nineoldandroids.animation.ObjectAnimator;
|
||||||
import java.util.ArrayList;
|
import java.util.ArrayList;
|
||||||
import java.util.List;
|
import java.util.List;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.animation.TransitionsTracker;
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
import org.mozilla.gecko.Tab;
|
import org.mozilla.gecko.Tab;
|
||||||
import org.mozilla.gecko.Tabs;
|
import org.mozilla.gecko.Tabs;
|
||||||
|
@ -134,6 +135,9 @@ public class TabStripView extends TwoWayView {
|
||||||
animatorSet.setDuration(ANIM_TIME_MS);
|
animatorSet.setDuration(ANIM_TIME_MS);
|
||||||
animatorSet.setInterpolator(ANIM_INTERPOLATOR);
|
animatorSet.setInterpolator(ANIM_INTERPOLATOR);
|
||||||
animatorSet.addListener(animatorListener);
|
animatorSet.addListener(animatorListener);
|
||||||
|
|
||||||
|
TransitionsTracker.track(animatorSet);
|
||||||
|
|
||||||
animatorSet.start();
|
animatorSet.start();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
@ -183,6 +187,9 @@ public class TabStripView extends TwoWayView {
|
||||||
animatorSet.setDuration(ANIM_TIME_MS);
|
animatorSet.setDuration(ANIM_TIME_MS);
|
||||||
animatorSet.setInterpolator(ANIM_INTERPOLATOR);
|
animatorSet.setInterpolator(ANIM_INTERPOLATOR);
|
||||||
animatorSet.addListener(animatorListener);
|
animatorSet.addListener(animatorListener);
|
||||||
|
|
||||||
|
TransitionsTracker.track(animatorSet);
|
||||||
|
|
||||||
animatorSet.start();
|
animatorSet.start();
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
|
|
|
@ -4,6 +4,7 @@
|
||||||
|
|
||||||
package org.mozilla.gecko.tabs;
|
package org.mozilla.gecko.tabs;
|
||||||
|
|
||||||
|
import org.mozilla.gecko.NewTabletUI;
|
||||||
import org.mozilla.gecko.R;
|
import org.mozilla.gecko.R;
|
||||||
import org.mozilla.gecko.Tab;
|
import org.mozilla.gecko.Tab;
|
||||||
import org.mozilla.gecko.widget.TabThumbnailWrapper;
|
import org.mozilla.gecko.widget.TabThumbnailWrapper;
|
||||||
|
@ -89,24 +90,26 @@ public class TabsLayoutItemView extends LinearLayout
|
||||||
mCloseButton = (ImageButton) findViewById(R.id.close);
|
mCloseButton = (ImageButton) findViewById(R.id.close);
|
||||||
mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
|
mThumbnailWrapper = (TabThumbnailWrapper) findViewById(R.id.wrapper);
|
||||||
|
|
||||||
|
if (NewTabletUI.isEnabled(getContext())) {
|
||||||
|
growCloseButtonHitArea();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
private void growCloseButtonHitArea() {
|
||||||
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
getViewTreeObserver().addOnPreDrawListener(new ViewTreeObserver.OnPreDrawListener() {
|
||||||
@Override
|
@Override
|
||||||
public boolean onPreDraw() {
|
public boolean onPreDraw() {
|
||||||
getViewTreeObserver().removeOnPreDrawListener(this);
|
getViewTreeObserver().removeOnPreDrawListener(this);
|
||||||
|
|
||||||
final Rect hitRect = new Rect();
|
|
||||||
mCloseButton.getHitRect(hitRect);
|
|
||||||
|
|
||||||
// Ideally we want the close button hit area to be 40x40dp but we are constrained by the height of the parent, so
|
// Ideally we want the close button hit area to be 40x40dp but we are constrained by the height of the parent, so
|
||||||
// we make it as tall as the parent view and 40dp across.
|
// we make it as tall as the parent view and 40dp across.
|
||||||
final int targetHitArea = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());;
|
final int targetHitArea = (int) TypedValue.applyDimension(TypedValue.COMPLEX_UNIT_DIP, 40, getResources().getDisplayMetrics());;
|
||||||
final View parent = ((View) mCloseButton.getParent());
|
|
||||||
|
|
||||||
|
|
||||||
|
final Rect hitRect = new Rect();
|
||||||
hitRect.top = 0;
|
hitRect.top = 0;
|
||||||
hitRect.right = getWidth();
|
hitRect.right = getWidth();
|
||||||
hitRect.left = getWidth() - targetHitArea;
|
hitRect.left = getWidth() - targetHitArea;
|
||||||
hitRect.bottom = parent.getHeight();
|
hitRect.bottom = targetHitArea;
|
||||||
|
|
||||||
setTouchDelegate(new TouchDelegate(hitRect, mCloseButton));
|
setTouchDelegate(new TouchDelegate(hitRect, mCloseButton));
|
||||||
|
|
||||||
|
|
|
@ -834,6 +834,17 @@ this.PlacesUtils = {
|
||||||
return null;
|
return null;
|
||||||
},
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets the href and the post data for a given keyword, if any.
|
||||||
|
*
|
||||||
|
* @param keyword
|
||||||
|
* The string keyword to look for.
|
||||||
|
* @return {Promise}
|
||||||
|
* @resolves to a { href, postData } object. Both properties evaluate to null
|
||||||
|
* if no keyword is found.
|
||||||
|
*/
|
||||||
|
promiseHrefAndPostDataForKeyword(keyword) KeywordsCache.promiseEntry(keyword),
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the URI (and any associated POST data) for a given keyword.
|
* Get the URI (and any associated POST data) for a given keyword.
|
||||||
* @param aKeyword string keyword
|
* @param aKeyword string keyword
|
||||||
|
@ -1965,6 +1976,114 @@ let GuidHelper = {
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Cache of bookmarks keywords, used to quickly resolve keyword => URL requests.
|
||||||
|
let KeywordsCache = {
|
||||||
|
/**
|
||||||
|
* Initializes the cache.
|
||||||
|
* Every method should check _initialized and, if false, yield _initialize().
|
||||||
|
*/
|
||||||
|
_initialized: false,
|
||||||
|
_initialize: Task.async(function* () {
|
||||||
|
// First populate the cache...
|
||||||
|
yield this._reloadCache();
|
||||||
|
|
||||||
|
// ...then observe changes to keep the cache up-to-date.
|
||||||
|
PlacesUtils.bookmarks.addObserver(this, false);
|
||||||
|
PlacesUtils.registerShutdownFunction(() => {
|
||||||
|
PlacesUtils.bookmarks.removeObserver(this);
|
||||||
|
});
|
||||||
|
|
||||||
|
this._initialized = true;
|
||||||
|
}),
|
||||||
|
|
||||||
|
// nsINavBookmarkObserver
|
||||||
|
// Manually updating the cache would be tricky because some notifications
|
||||||
|
// don't report the original bookmark url and we also keep urls sorted by
|
||||||
|
// last modified. Since changing a keyword-ed bookmark is a rare event,
|
||||||
|
// it's easier to reload the cache.
|
||||||
|
onItemChanged(itemId, property, isAnno, val, lastModified, type,
|
||||||
|
parentId, guid, parentGuid) {
|
||||||
|
if (property == "keyword" || property == this.POST_DATA_ANNO ||
|
||||||
|
this._keywordedGuids.has(guid)) {
|
||||||
|
// Since this cache is used in hot paths, it should be readily available
|
||||||
|
// as fast as possible.
|
||||||
|
this._reloadCache().catch(Cu.reportError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
onItemRemoved(itemId, parentId, index, type, uri, guid, parentGuid) {
|
||||||
|
if (this._keywordedGuids.has(guid)) {
|
||||||
|
// Since this cache is used in hot paths, it should be readily available
|
||||||
|
// as fast as possible.
|
||||||
|
this._reloadCache().catch(Cu.reportError);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
QueryInterface: XPCOMUtils.generateQI([ Ci.nsINavBookmarkObserver ]),
|
||||||
|
__noSuchMethod__() {}, // Catch all remaining onItem* methods.
|
||||||
|
|
||||||
|
// Maps an { href, postData } object to each keyword.
|
||||||
|
// Even if a keyword may be associated to multiple URLs, only the last
|
||||||
|
// modified bookmark href is retained here.
|
||||||
|
_urlDataForKeyword: null,
|
||||||
|
// Tracks GUIDs having a keyword.
|
||||||
|
_keywordedGuids: null,
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reloads the cache.
|
||||||
|
*/
|
||||||
|
_reloadPromise: null,
|
||||||
|
_reloadCache() {
|
||||||
|
return this._reloadPromise = Task.spawn(function* () {
|
||||||
|
let db = yield PlacesUtils.promiseDBConnection();
|
||||||
|
let rows = yield db.execute(
|
||||||
|
`/* do not warn (bug no) - there is no index on keyword_id */
|
||||||
|
SELECT b.id, b.guid, h.url, k.keyword FROM moz_bookmarks b
|
||||||
|
JOIN moz_places h ON h.id = b.fk
|
||||||
|
JOIN moz_keywords k ON k.id = b.keyword_id
|
||||||
|
ORDER BY b.lastModified DESC
|
||||||
|
`);
|
||||||
|
|
||||||
|
this._urlDataForKeyword = new Map();
|
||||||
|
this._keywordedGuids = new Set();
|
||||||
|
|
||||||
|
for (let row of rows) {
|
||||||
|
let guid = row.getResultByName("guid");
|
||||||
|
this._keywordedGuids.add(guid);
|
||||||
|
|
||||||
|
let keyword = row.getResultByName("keyword");
|
||||||
|
// Only keep the most recent href.
|
||||||
|
let urlData = this._urlDataForKeyword.get(keyword);
|
||||||
|
if (urlData)
|
||||||
|
continue;
|
||||||
|
|
||||||
|
let id = row.getResultByName("id");
|
||||||
|
let href = row.getResultByName("url");
|
||||||
|
let postData = PlacesUtils.getPostDataForBookmark(id);
|
||||||
|
this._urlDataForKeyword.set(keyword, { href, postData });
|
||||||
|
}
|
||||||
|
}.bind(this)).then(() => {
|
||||||
|
this._reloadPromise = null;
|
||||||
|
});
|
||||||
|
},
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a { href, postData } entry for the given keyword.
|
||||||
|
*
|
||||||
|
* @param keyword
|
||||||
|
* The keyword to look for.
|
||||||
|
* @return {promise}
|
||||||
|
* @resolves when the fetching is complete.
|
||||||
|
*/
|
||||||
|
promiseEntry: Task.async(function* (keyword) {
|
||||||
|
// We could yield regardless and do the checks internally, but that would
|
||||||
|
// waste at least a couple ticks and this can be used on hot paths.
|
||||||
|
if (!this._initialized)
|
||||||
|
yield this._initialize();
|
||||||
|
if (this._reloadPromise)
|
||||||
|
yield this._reloadPromise;
|
||||||
|
return this._urlDataForKeyword.get(keyword) || { href: null, postData: null };
|
||||||
|
}),
|
||||||
|
};
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//// Transactions handlers.
|
//// Transactions handlers.
|
||||||
|
|
||||||
|
|
|
@ -768,7 +768,7 @@ Search.prototype = {
|
||||||
let hasFirstResult = false;
|
let hasFirstResult = false;
|
||||||
|
|
||||||
if (this._searchTokens.length > 0 &&
|
if (this._searchTokens.length > 0 &&
|
||||||
PlacesUtils.bookmarks.getURIForKeyword(this._searchTokens[0])) {
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword(this._searchTokens[0])).href) {
|
||||||
// This may be a keyword of a bookmark.
|
// This may be a keyword of a bookmark.
|
||||||
queries.unshift(this._keywordQuery);
|
queries.unshift(this._keywordQuery);
|
||||||
hasFirstResult = true;
|
hasFirstResult = true;
|
||||||
|
|
|
@ -19,8 +19,6 @@
|
||||||
|
|
||||||
#include "GeckoProfiler.h"
|
#include "GeckoProfiler.h"
|
||||||
|
|
||||||
#define BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH 32
|
|
||||||
|
|
||||||
using namespace mozilla;
|
using namespace mozilla;
|
||||||
|
|
||||||
// These columns sit to the right of the kGetInfoIndex_* columns.
|
// These columns sit to the right of the kGetInfoIndex_* columns.
|
||||||
|
@ -40,25 +38,6 @@ PLACES_FACTORY_SINGLETON_IMPLEMENTATION(nsNavBookmarks, gBookmarksService)
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
struct keywordSearchData
|
|
||||||
{
|
|
||||||
int64_t itemId;
|
|
||||||
nsString keyword;
|
|
||||||
};
|
|
||||||
|
|
||||||
PLDHashOperator
|
|
||||||
SearchBookmarkForKeyword(nsTrimInt64HashKey::KeyType aKey,
|
|
||||||
const nsString aValue,
|
|
||||||
void* aUserArg)
|
|
||||||
{
|
|
||||||
keywordSearchData* data = reinterpret_cast<keywordSearchData*>(aUserArg);
|
|
||||||
if (data->keyword.Equals(aValue)) {
|
|
||||||
data->itemId = aKey;
|
|
||||||
return PL_DHASH_STOP;
|
|
||||||
}
|
|
||||||
return PL_DHASH_NEXT;
|
|
||||||
}
|
|
||||||
|
|
||||||
template<typename Method, typename DataType>
|
template<typename Method, typename DataType>
|
||||||
class AsyncGetBookmarksForURI : public AsyncStatementCallback
|
class AsyncGetBookmarksForURI : public AsyncStatementCallback
|
||||||
{
|
{
|
||||||
|
@ -143,8 +122,6 @@ nsNavBookmarks::nsNavBookmarks()
|
||||||
, mCanNotify(false)
|
, mCanNotify(false)
|
||||||
, mCacheObservers("bookmark-observers")
|
, mCacheObservers("bookmark-observers")
|
||||||
, mBatching(false)
|
, mBatching(false)
|
||||||
, mBookmarkToKeywordHash(BOOKMARKS_TO_KEYWORDS_INITIAL_CACHE_LENGTH)
|
|
||||||
, mBookmarkToKeywordHashInitialized(false)
|
|
||||||
{
|
{
|
||||||
NS_ASSERTION(!gBookmarksService,
|
NS_ASSERTION(!gBookmarksService,
|
||||||
"Attempting to create two instances of the service!");
|
"Attempting to create two instances of the service!");
|
||||||
|
@ -646,7 +623,7 @@ nsNavBookmarks::RemoveItem(int64_t aItemId)
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
rv = UpdateKeywordsHashForRemovedBookmark(aItemId);
|
rv = removeOrphanKeywords();
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
// A broken url should not interrupt the removal process.
|
// A broken url should not interrupt the removal process.
|
||||||
|
@ -1119,7 +1096,7 @@ nsNavBookmarks::RemoveFolderChildren(int64_t aFolderId)
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
}
|
}
|
||||||
|
|
||||||
rv = UpdateKeywordsHashForRemovedBookmark(child.id);
|
rv = removeOrphanKeywords();
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -2255,25 +2232,12 @@ nsNavBookmarks::SetItemIndex(int64_t aItemId, int32_t aNewIndex)
|
||||||
|
|
||||||
|
|
||||||
nsresult
|
nsresult
|
||||||
nsNavBookmarks::UpdateKeywordsHashForRemovedBookmark(int64_t aItemId)
|
nsNavBookmarks::removeOrphanKeywords()
|
||||||
{
|
{
|
||||||
nsAutoString keyword;
|
|
||||||
if (NS_SUCCEEDED(GetKeywordForBookmark(aItemId, keyword)) &&
|
|
||||||
!keyword.IsEmpty()) {
|
|
||||||
nsresult rv = EnsureKeywordsHash();
|
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
|
||||||
mBookmarkToKeywordHash.Remove(aItemId);
|
|
||||||
|
|
||||||
// If the keyword is unused, remove it from the database.
|
// If the keyword is unused, remove it from the database.
|
||||||
keywordSearchData searchData;
|
|
||||||
searchData.keyword.Assign(keyword);
|
|
||||||
searchData.itemId = -1;
|
|
||||||
mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData);
|
|
||||||
if (searchData.itemId == -1) {
|
|
||||||
nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
|
nsCOMPtr<mozIStorageAsyncStatement> stmt = mDB->GetAsyncStatement(
|
||||||
"DELETE FROM moz_keywords "
|
"DELETE FROM moz_keywords "
|
||||||
"WHERE keyword = :keyword "
|
"WHERE NOT EXISTS ( "
|
||||||
"AND NOT EXISTS ( "
|
|
||||||
"SELECT id "
|
"SELECT id "
|
||||||
"FROM moz_bookmarks "
|
"FROM moz_bookmarks "
|
||||||
"WHERE keyword_id = moz_keywords.id "
|
"WHERE keyword_id = moz_keywords.id "
|
||||||
|
@ -2281,13 +2245,10 @@ nsNavBookmarks::UpdateKeywordsHashForRemovedBookmark(int64_t aItemId)
|
||||||
);
|
);
|
||||||
NS_ENSURE_STATE(stmt);
|
NS_ENSURE_STATE(stmt);
|
||||||
|
|
||||||
rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
|
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
|
||||||
nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
|
nsCOMPtr<mozIStoragePendingStatement> pendingStmt;
|
||||||
rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt));
|
nsresult rv = stmt->ExecuteAsync(nullptr, getter_AddRefs(pendingStmt));
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
}
|
|
||||||
}
|
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2303,9 +2264,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
|
||||||
nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
|
nsresult rv = FetchItemInfo(aBookmarkId, bookmark);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
rv = EnsureKeywordsHash();
|
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
|
||||||
|
|
||||||
// Shortcuts are always lowercased internally.
|
// Shortcuts are always lowercased internally.
|
||||||
nsAutoString keyword(aUserCasedKeyword);
|
nsAutoString keyword(aUserCasedKeyword);
|
||||||
ToLowerCase(keyword);
|
ToLowerCase(keyword);
|
||||||
|
@ -2331,8 +2289,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
|
||||||
mozStorageStatementScoper updateBookmarkScoper(updateBookmarkStmt);
|
mozStorageStatementScoper updateBookmarkScoper(updateBookmarkStmt);
|
||||||
|
|
||||||
if (keyword.IsEmpty()) {
|
if (keyword.IsEmpty()) {
|
||||||
// Remove keyword association from the hash.
|
|
||||||
mBookmarkToKeywordHash.Remove(bookmark.id);
|
|
||||||
rv = updateBookmarkStmt->BindNullByName(NS_LITERAL_CSTRING("keyword"));
|
rv = updateBookmarkStmt->BindNullByName(NS_LITERAL_CSTRING("keyword"));
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
|
@ -2350,10 +2306,6 @@ nsNavBookmarks::SetKeywordForBookmark(int64_t aBookmarkId,
|
||||||
rv = newKeywordStmt->Execute();
|
rv = newKeywordStmt->Execute();
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
// Add new keyword association to the hash, removing the old one if needed.
|
|
||||||
if (!oldKeyword.IsEmpty())
|
|
||||||
mBookmarkToKeywordHash.Remove(bookmark.id);
|
|
||||||
mBookmarkToKeywordHash.Put(bookmark.id, keyword);
|
|
||||||
rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
|
rv = updateBookmarkStmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
|
||||||
}
|
}
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
@ -2411,12 +2363,12 @@ nsNavBookmarks::GetKeywordForURI(nsIURI* aURI, nsAString& aKeyword)
|
||||||
rv = stmt->ExecuteStep(&hasMore);
|
rv = stmt->ExecuteStep(&hasMore);
|
||||||
if (NS_FAILED(rv) || !hasMore) {
|
if (NS_FAILED(rv) || !hasMore) {
|
||||||
aKeyword.SetIsVoid(true);
|
aKeyword.SetIsVoid(true);
|
||||||
return NS_OK; // not found: return void keyword string
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
// found, get the keyword
|
|
||||||
rv = stmt->GetString(0, aKeyword);
|
rv = stmt->GetString(0, aKeyword);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -2427,16 +2379,28 @@ nsNavBookmarks::GetKeywordForBookmark(int64_t aBookmarkId, nsAString& aKeyword)
|
||||||
NS_ENSURE_ARG_MIN(aBookmarkId, 1);
|
NS_ENSURE_ARG_MIN(aBookmarkId, 1);
|
||||||
aKeyword.Truncate(0);
|
aKeyword.Truncate(0);
|
||||||
|
|
||||||
nsresult rv = EnsureKeywordsHash();
|
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||||
|
"/* do not warn (bug no) - there is no index on keyword_id) */ "
|
||||||
|
"SELECT k.keyword "
|
||||||
|
"FROM moz_bookmarks b "
|
||||||
|
"JOIN moz_keywords k ON k.id = b.keyword_id "
|
||||||
|
"WHERE b.id = :id "
|
||||||
|
);
|
||||||
|
NS_ENSURE_STATE(stmt);
|
||||||
|
mozStorageStatementScoper scoper(stmt);
|
||||||
|
|
||||||
|
nsresult rv = stmt->BindInt64ByName(NS_LITERAL_CSTRING("id"), aBookmarkId);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
nsAutoString keyword;
|
bool hasMore = false;
|
||||||
if (!mBookmarkToKeywordHash.Get(aBookmarkId, &keyword)) {
|
rv = stmt->ExecuteStep(&hasMore);
|
||||||
|
if (NS_FAILED(rv) || !hasMore) {
|
||||||
aKeyword.SetIsVoid(true);
|
aKeyword.SetIsVoid(true);
|
||||||
|
return NS_OK;
|
||||||
}
|
}
|
||||||
else {
|
|
||||||
aKeyword.Assign(keyword);
|
rv = stmt->GetString(0, aKeyword);
|
||||||
}
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
@ -2454,53 +2418,33 @@ nsNavBookmarks::GetURIForKeyword(const nsAString& aUserCasedKeyword,
|
||||||
nsAutoString keyword(aUserCasedKeyword);
|
nsAutoString keyword(aUserCasedKeyword);
|
||||||
ToLowerCase(keyword);
|
ToLowerCase(keyword);
|
||||||
|
|
||||||
nsresult rv = EnsureKeywordsHash();
|
nsCOMPtr<mozIStorageStatement> stmt = mDB->GetStatement(
|
||||||
|
"/* do not warn (bug no) - there is no index on keyword_id) */ "
|
||||||
|
"SELECT url FROM moz_keywords k "
|
||||||
|
"JOIN moz_bookmarks b ON b.keyword_id = k.id "
|
||||||
|
"JOIN moz_places h ON b.fk = h.id "
|
||||||
|
"WHERE k.keyword = :keyword "
|
||||||
|
"ORDER BY b.dateAdded DESC"
|
||||||
|
);
|
||||||
|
NS_ENSURE_STATE(stmt);
|
||||||
|
mozStorageStatementScoper scoper(stmt);
|
||||||
|
|
||||||
|
nsresult rv = stmt->BindStringByName(NS_LITERAL_CSTRING("keyword"), keyword);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
keywordSearchData searchData;
|
bool hasMore = false;
|
||||||
searchData.keyword.Assign(keyword);
|
rv = stmt->ExecuteStep(&hasMore);
|
||||||
searchData.itemId = -1;
|
if (NS_FAILED(rv) || !hasMore) {
|
||||||
mBookmarkToKeywordHash.EnumerateRead(SearchBookmarkForKeyword, &searchData);
|
|
||||||
|
|
||||||
if (searchData.itemId == -1) {
|
|
||||||
// Not found.
|
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
rv = GetBookmarkURI(searchData.itemId, aURI);
|
nsCString url;
|
||||||
|
rv = stmt->GetUTF8String(0, url);
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
return NS_OK;
|
rv = NS_NewURI(aURI, url);
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
nsresult
|
|
||||||
nsNavBookmarks::EnsureKeywordsHash() {
|
|
||||||
if (mBookmarkToKeywordHashInitialized) {
|
|
||||||
return NS_OK;
|
|
||||||
}
|
|
||||||
mBookmarkToKeywordHashInitialized = true;
|
|
||||||
|
|
||||||
nsCOMPtr<mozIStorageStatement> stmt;
|
|
||||||
nsresult rv = mDB->MainConn()->CreateStatement(NS_LITERAL_CSTRING(
|
|
||||||
"SELECT b.id, k.keyword "
|
|
||||||
"FROM moz_bookmarks b "
|
|
||||||
"JOIN moz_keywords k ON k.id = b.keyword_id "
|
|
||||||
), getter_AddRefs(stmt));
|
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
NS_ENSURE_SUCCESS(rv, rv);
|
||||||
|
|
||||||
bool hasMore;
|
|
||||||
while (NS_SUCCEEDED(stmt->ExecuteStep(&hasMore)) && hasMore) {
|
|
||||||
int64_t itemId;
|
|
||||||
rv = stmt->GetInt64(0, &itemId);
|
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
|
||||||
nsAutoString keyword;
|
|
||||||
rv = stmt->GetString(1, keyword);
|
|
||||||
NS_ENSURE_SUCCESS(rv, rv);
|
|
||||||
|
|
||||||
mBookmarkToKeywordHash.Put(itemId, keyword);
|
|
||||||
}
|
|
||||||
|
|
||||||
return NS_OK;
|
return NS_OK;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -422,20 +422,9 @@ private:
|
||||||
bool mBatching;
|
bool mBatching;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Always call EnsureKeywordsHash() and check it for errors before actually
|
* Removes orphan keywords.
|
||||||
* using the hash. Internal keyword methods are already doing that.
|
|
||||||
*/
|
*/
|
||||||
nsresult EnsureKeywordsHash();
|
nsresult removeOrphanKeywords();
|
||||||
nsDataHashtable<nsTrimInt64HashKey, nsString> mBookmarkToKeywordHash;
|
|
||||||
bool mBookmarkToKeywordHashInitialized;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This function must be called every time a bookmark is removed.
|
|
||||||
*
|
|
||||||
* @param aURI
|
|
||||||
* Uri to test.
|
|
||||||
*/
|
|
||||||
nsresult UpdateKeywordsHashForRemovedBookmark(int64_t aItemId);
|
|
||||||
};
|
};
|
||||||
|
|
||||||
#endif // nsNavBookmarks_h_
|
#endif // nsNavBookmarks_h_
|
||||||
|
|
|
@ -12,6 +12,8 @@ XPCOMUtils.defineLazyModuleGetter(this, "TelemetryStopwatch",
|
||||||
"resource://gre/modules/TelemetryStopwatch.jsm");
|
"resource://gre/modules/TelemetryStopwatch.jsm");
|
||||||
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
|
||||||
"resource://gre/modules/NetUtil.jsm");
|
"resource://gre/modules/NetUtil.jsm");
|
||||||
|
XPCOMUtils.defineLazyModuleGetter(this, "Task",
|
||||||
|
"resource://gre/modules/Task.jsm");
|
||||||
|
|
||||||
////////////////////////////////////////////////////////////////////////////////
|
////////////////////////////////////////////////////////////////////////////////
|
||||||
//// Constants
|
//// Constants
|
||||||
|
@ -1447,12 +1449,13 @@ urlInlineComplete.prototype = {
|
||||||
|
|
||||||
this._listener = aListener;
|
this._listener = aListener;
|
||||||
|
|
||||||
|
Task.spawn(function* () {
|
||||||
// Don't autoFill if the search term is recognized as a keyword, otherwise
|
// Don't autoFill if the search term is recognized as a keyword, otherwise
|
||||||
// it will override default keywords behavior. Note that keywords are
|
// it will override default keywords behavior. Note that keywords are
|
||||||
// hashed on first use, so while the first query may delay a little bit,
|
// hashed on first use, so while the first query may delay a little bit,
|
||||||
// next ones will just hit the memory hash.
|
// next ones will just hit the memory hash.
|
||||||
if (this._currentSearchString.length == 0 || !this._db ||
|
if (this._currentSearchString.length == 0 || !this._db ||
|
||||||
PlacesUtils.bookmarks.getURIForKeyword(this._currentSearchString)) {
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword(this._currentSearchString)).href) {
|
||||||
this._finishSearch();
|
this._finishSearch();
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -1515,6 +1518,7 @@ urlInlineComplete.prototype = {
|
||||||
}
|
}
|
||||||
}, this._db);
|
}, this._db);
|
||||||
this._pendingQuery = wrapper.executeAsync([query]);
|
this._pendingQuery = wrapper.executeAsync([query]);
|
||||||
|
}.bind(this));
|
||||||
},
|
},
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
|
|
@ -0,0 +1,117 @@
|
||||||
|
/* Any copyright is dedicated to the Public Domain.
|
||||||
|
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||||
|
|
||||||
|
add_task(function* test_no_keyword() {
|
||||||
|
Assert.deepEqual({ href: null, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_add_remove() {
|
||||||
|
let item1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||||
|
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||||
|
url: "http://example1.com/",
|
||||||
|
keyword: "test" });
|
||||||
|
Assert.deepEqual({ href: item1.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item1.url.href);
|
||||||
|
|
||||||
|
// Add a second url for the same keyword, since it's newer it should be
|
||||||
|
// returned.
|
||||||
|
let item2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||||
|
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||||
|
url: "http://example2.com/",
|
||||||
|
keyword: "test" });
|
||||||
|
Assert.deepEqual({ href: item2.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item2.url.href);
|
||||||
|
|
||||||
|
// Now remove item2, should return item1 again.
|
||||||
|
yield PlacesUtils.bookmarks.remove(item2);
|
||||||
|
Assert.deepEqual({ href: item1.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item1.url.href);
|
||||||
|
|
||||||
|
// Now remove item1, should return null again.
|
||||||
|
yield PlacesUtils.bookmarks.remove(item1);
|
||||||
|
Assert.deepEqual({ href: null, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should not exist");
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_change_url() {
|
||||||
|
let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||||
|
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||||
|
url: "http://example.com/",
|
||||||
|
keyword: "test" });
|
||||||
|
Assert.deepEqual({ href: item.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item.url.href);
|
||||||
|
|
||||||
|
// Change the bookmark url.
|
||||||
|
let updatedItem = yield PlacesUtils.bookmarks.update({ guid: item.guid,
|
||||||
|
url: "http://example2.com" });
|
||||||
|
Assert.deepEqual({ href: updatedItem.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + updatedItem.url.href);
|
||||||
|
yield PlacesUtils.bookmarks.remove(updatedItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_change_keyword() {
|
||||||
|
let item = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||||
|
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||||
|
url: "http://example.com/",
|
||||||
|
keyword: "test" });
|
||||||
|
Assert.deepEqual({ href: item.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item.url.href);
|
||||||
|
|
||||||
|
// Change the bookmark keywprd.
|
||||||
|
let updatedItem = yield PlacesUtils.bookmarks.update({ guid: item.guid,
|
||||||
|
keyword: "test2" });
|
||||||
|
Assert.deepEqual({ href: null, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should not exist");
|
||||||
|
Assert.deepEqual({ href: updatedItem.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test2")),
|
||||||
|
"Keyword 'test' should point to " + updatedItem.url.href);
|
||||||
|
|
||||||
|
// Remove the bookmark keyword.
|
||||||
|
updatedItem = yield PlacesUtils.bookmarks.update({ guid: item.guid,
|
||||||
|
keyword: "" });
|
||||||
|
Assert.deepEqual({ href: null, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should not exist");
|
||||||
|
Assert.deepEqual({ href: null, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test2")),
|
||||||
|
"Keyword 'test' should not exist");
|
||||||
|
yield PlacesUtils.bookmarks.remove(updatedItem);
|
||||||
|
});
|
||||||
|
|
||||||
|
add_task(function* test_postData() {
|
||||||
|
let item1 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||||
|
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||||
|
url: "http://example1.com/",
|
||||||
|
keyword: "test" });
|
||||||
|
let itemId1 = yield PlacesUtils.promiseItemId(item1.guid);
|
||||||
|
PlacesUtils.setPostDataForBookmark(itemId1, "testData");
|
||||||
|
Assert.deepEqual({ href: item1.url.href, postData: "testData" },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item1.url.href);
|
||||||
|
|
||||||
|
// Add a second url for the same keyword, but without postData.
|
||||||
|
let item2 = yield PlacesUtils.bookmarks.insert({ type: PlacesUtils.bookmarks.TYPE_BOOKMARK,
|
||||||
|
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
|
||||||
|
url: "http://example2.com/",
|
||||||
|
keyword: "test" });
|
||||||
|
Assert.deepEqual({ href: item2.url.href, postData: null },
|
||||||
|
(yield PlacesUtils.promiseHrefAndPostDataForKeyword("test")),
|
||||||
|
"Keyword 'test' should point to " + item2.url.href);
|
||||||
|
|
||||||
|
yield PlacesUtils.bookmarks.remove(item1);
|
||||||
|
yield PlacesUtils.bookmarks.remove(item2);
|
||||||
|
});
|
||||||
|
|
||||||
|
function run_test() {
|
||||||
|
run_next_test();
|
||||||
|
}
|
|
@ -119,6 +119,7 @@ skip-if = true
|
||||||
[test_PlacesSearchAutocompleteProvider.js]
|
[test_PlacesSearchAutocompleteProvider.js]
|
||||||
[test_PlacesUtils_asyncGetBookmarkIds.js]
|
[test_PlacesUtils_asyncGetBookmarkIds.js]
|
||||||
[test_PlacesUtils_lazyobservers.js]
|
[test_PlacesUtils_lazyobservers.js]
|
||||||
|
[test_PlacesUtils_promiseHrefAndPostDataForKeyword.js]
|
||||||
[test_placesTxn.js]
|
[test_placesTxn.js]
|
||||||
[test_preventive_maintenance.js]
|
[test_preventive_maintenance.js]
|
||||||
# Bug 676989: test hangs consistently on Android
|
# Bug 676989: test hangs consistently on Android
|
||||||
|
|
|
@ -148,34 +148,6 @@ function goOnEvent(aNode, aEvent)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function visitLink(aEvent) {
|
|
||||||
var node = aEvent.target;
|
|
||||||
while (node.nodeType != Node.ELEMENT_NODE)
|
|
||||||
node = node.parentNode;
|
|
||||||
var url = node.getAttribute("link");
|
|
||||||
if (!url)
|
|
||||||
return;
|
|
||||||
|
|
||||||
var protocolSvc = Components.classes["@mozilla.org/uriloader/external-protocol-service;1"]
|
|
||||||
.getService(Components.interfaces.nsIExternalProtocolService);
|
|
||||||
var ioService = Components.classes["@mozilla.org/network/io-service;1"]
|
|
||||||
.getService(Components.interfaces.nsIIOService);
|
|
||||||
var uri = ioService.newURI(url, null, null);
|
|
||||||
|
|
||||||
// if the scheme is not an exposed protocol, then opening this link
|
|
||||||
// should be deferred to the system's external protocol handler
|
|
||||||
if (protocolSvc.isExposedProtocol(uri.scheme)) {
|
|
||||||
var win = window.top;
|
|
||||||
if (win instanceof Components.interfaces.nsIDOMChromeWindow) {
|
|
||||||
while (win.opener && !win.opener.closed)
|
|
||||||
win = win.opener;
|
|
||||||
}
|
|
||||||
win.open(uri.spec);
|
|
||||||
}
|
|
||||||
else
|
|
||||||
protocolSvc.loadUrl(uri);
|
|
||||||
}
|
|
||||||
|
|
||||||
function setTooltipText(aID, aTooltipText)
|
function setTooltipText(aID, aTooltipText)
|
||||||
{
|
{
|
||||||
var element = document.getElementById(aID);
|
var element = document.getElementById(aID);
|
||||||
|
|
Загрузка…
Ссылка в новой задаче