Merge fx-team to m-c. a=merge

This commit is contained in:
Ryan VanderMeulen 2014-09-22 14:57:14 -04:00
commit 6d140e81d7
58 changed files with 1561 additions and 454 deletions

View File

@ -697,12 +697,15 @@ pref("plugin.state.ciscowebcommunicator", 2);
pref("plugin.state.npmcafeemss", 2);
#endif
// Cisco VGConnect for directv.com, bug 981403
// Cisco VGConnect for directv.com, bug 981403 & bug 1051772
#ifdef XP_WIN
pref("plugin.state.npplayerplugin", 2);
#endif
#ifdef XP_MACOSX
pref("plugin.state.playerplugin", 2);
pref("plugin.state.playerplugin.dtv", 2);
pref("plugin.state.playerplugin.ciscodrm", 2);
pref("plugin.state.playerplugin.charter", 2);
#endif
// Cisco Jabber Client, bug 981905
@ -843,6 +846,15 @@ pref("plugin.state.personalplugin", 2);
pref("plugin.state.libplugins", 2);
#endif
// Novell iPrint Client, bug 1036693
#ifdef XP_WIN
pref("plugin.state.npnipp", 2);
pref("plugin.state.npnisp", 2);
#endif
#ifdef XP_MACOSX
pref("plugin.state.iprint", 2);
#endif
#ifdef XP_MACOSX
pref("browser.preferences.animateFadeIn", true);
#else

View File

@ -114,7 +114,7 @@ loop.Client = (function($) {
} else {
sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST;
}
this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST",
{callerId: nickname},
function (error, responseText) {

View File

@ -406,6 +406,7 @@ loop.conversation = (function(OT, mozL10n) {
/*jshint newcap:false*/
this.loadReactComponent(sharedViews.ConversationView({
initiate: true,
sdk: OT,
model: this._conversation,
video: {enabled: videoStream}
@ -440,7 +441,8 @@ loop.conversation = (function(OT, mozL10n) {
});
this.loadReactComponent(sharedViews.FeedbackView({
feedbackApiClient: feedbackClient
feedbackApiClient: feedbackClient,
onAfterFeedbackReceived: window.close.bind(window)
}));
}
});

View File

@ -406,6 +406,7 @@ loop.conversation = (function(OT, mozL10n) {
/*jshint newcap:false*/
this.loadReactComponent(sharedViews.ConversationView({
initiate: true,
sdk: OT,
model: this._conversation,
video: {enabled: videoStream}
@ -440,7 +441,8 @@ loop.conversation = (function(OT, mozL10n) {
});
this.loadReactComponent(sharedViews.FeedbackView({
feedbackApiClient: feedbackClient
feedbackApiClient: feedbackClient,
onAfterFeedbackReceived: window.close.bind(window)
}));
}
});

View File

@ -51,7 +51,8 @@ loop.FeedbackAPIClient = (function($, _) {
"platform",
"version",
"channel",
"user_agent"],
"user_agent",
"url"],
/**
* Creates a formatted payload object compliant with the Feedback API spec

View File

@ -30,11 +30,12 @@ loop.shared.views = (function(_, OT, l10n) {
scope: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired,
action: React.PropTypes.func.isRequired,
enabled: React.PropTypes.bool.isRequired
enabled: React.PropTypes.bool.isRequired,
visible: React.PropTypes.bool.isRequired
},
getDefaultProps: function() {
return {enabled: true};
return {enabled: true, visible: true};
},
handleClick: function() {
@ -48,7 +49,8 @@ loop.shared.views = (function(_, OT, l10n) {
"btn": true,
"media-control": true,
"local-media": this.props.scope === "local",
"muted": !this.props.enabled
"muted": !this.props.enabled,
"hide": !this.props.visible
};
classesObj["btn-mute-" + this.props.type] = true;
return cx(classesObj);
@ -78,8 +80,8 @@ loop.shared.views = (function(_, OT, l10n) {
var ConversationToolbar = React.createClass({displayName: 'ConversationToolbar',
getDefaultProps: function() {
return {
video: {enabled: true},
audio: {enabled: true}
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
@ -103,7 +105,7 @@ loop.shared.views = (function(_, OT, l10n) {
},
render: function() {
/* jshint ignore:start */
var cx = React.addons.classSet;
return (
React.DOM.ul({className: "conversation-toolbar"},
React.DOM.li({className: "conversation-toolbar-btn-box"},
@ -115,25 +117,31 @@ loop.shared.views = (function(_, OT, l10n) {
React.DOM.li({className: "conversation-toolbar-btn-box"},
MediaControlButton({action: this.handleToggleVideo,
enabled: this.props.video.enabled,
visible: this.props.video.visible,
scope: "local", type: "video"})
),
React.DOM.li({className: "conversation-toolbar-btn-box"},
MediaControlButton({action: this.handleToggleAudio,
enabled: this.props.audio.enabled,
visible: this.props.audio.visible,
scope: "local", type: "audio"})
)
)
);
/* jshint ignore:end */
}
});
/**
* Conversation view.
*/
var ConversationView = React.createClass({displayName: 'ConversationView',
mixins: [Backbone.Events],
propTypes: {
sdk: React.PropTypes.object.isRequired,
model: React.PropTypes.object.isRequired
video: React.PropTypes.object,
audio: React.PropTypes.object,
initiate: React.PropTypes.bool
},
// height set to 100%" to fix video layout on Google Chrome
@ -149,10 +157,11 @@ loop.shared.views = (function(_, OT, l10n) {
}
},
getInitialProps: function() {
getDefaultProps: function() {
return {
video: {enabled: true},
audio: {enabled: true}
initiate: true,
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
@ -164,26 +173,30 @@ loop.shared.views = (function(_, OT, l10n) {
},
componentWillMount: function() {
this.publisherConfig.publishVideo = this.props.video.enabled;
if (this.props.initiate) {
this.publisherConfig.publishVideo = this.props.video.enabled;
}
},
componentDidMount: function() {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "),
this.stopPublishing);
this.props.model.startSession();
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "),
this.stopPublishing);
this.props.model.startSession();
}
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* */
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
},
@ -282,10 +295,12 @@ loop.shared.views = (function(_, OT, l10n) {
* Unpublishes local stream.
*/
stopPublishing: function() {
// Unregister listeners for publisher events
this.stopListening(this.publisher);
if (this.publisher) {
// Unregister listeners for publisher events
this.stopListening(this.publisher);
this.props.model.session.unpublish(this.publisher);
this.props.model.session.unpublish(this.publisher);
}
},
render: function() {
@ -362,7 +377,7 @@ loop.shared.views = (function(_, OT, l10n) {
return {category: "", description: ""};
},
getInitialProps: function() {
getDefaultProps: function() {
return {pending: false};
},
@ -467,8 +482,16 @@ loop.shared.views = (function(_, OT, l10n) {
/**
* 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};
},
@ -488,7 +511,9 @@ loop.shared.views = (function(_, OT, l10n) {
render: function() {
if (this.state.countdown < 1) {
clearInterval(this._timer);
window.close();
if (this.props.onAfterFeedbackReceived) {
this.props.onAfterFeedbackReceived();
}
}
return (
FeedbackLayout({title: l10n.get("feedback_thank_you_heading")},
@ -509,6 +534,7 @@ loop.shared.views = (function(_, OT, l10n) {
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"])
},
@ -517,7 +543,7 @@ loop.shared.views = (function(_, OT, l10n) {
return {pending: false, step: this.props.step || "start"};
},
getInitialProps: function() {
getDefaultProps: function() {
return {step: "start"};
},
@ -552,7 +578,10 @@ loop.shared.views = (function(_, OT, l10n) {
render: function() {
switch(this.state.step) {
case "finished":
return FeedbackReceived(null);
return (
FeedbackReceived({
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived})
);
case "form":
return FeedbackForm({feedbackApiClient: this.props.feedbackApiClient,
sendFeedback: this.sendFeedback,

View File

@ -30,11 +30,12 @@ loop.shared.views = (function(_, OT, l10n) {
scope: React.PropTypes.string.isRequired,
type: React.PropTypes.string.isRequired,
action: React.PropTypes.func.isRequired,
enabled: React.PropTypes.bool.isRequired
enabled: React.PropTypes.bool.isRequired,
visible: React.PropTypes.bool.isRequired
},
getDefaultProps: function() {
return {enabled: true};
return {enabled: true, visible: true};
},
handleClick: function() {
@ -48,7 +49,8 @@ loop.shared.views = (function(_, OT, l10n) {
"btn": true,
"media-control": true,
"local-media": this.props.scope === "local",
"muted": !this.props.enabled
"muted": !this.props.enabled,
"hide": !this.props.visible
};
classesObj["btn-mute-" + this.props.type] = true;
return cx(classesObj);
@ -78,8 +80,8 @@ loop.shared.views = (function(_, OT, l10n) {
var ConversationToolbar = React.createClass({
getDefaultProps: function() {
return {
video: {enabled: true},
audio: {enabled: true}
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
@ -103,7 +105,7 @@ loop.shared.views = (function(_, OT, l10n) {
},
render: function() {
/* jshint ignore:start */
var cx = React.addons.classSet;
return (
<ul className="conversation-toolbar">
<li className="conversation-toolbar-btn-box">
@ -115,25 +117,31 @@ loop.shared.views = (function(_, OT, l10n) {
<li className="conversation-toolbar-btn-box">
<MediaControlButton action={this.handleToggleVideo}
enabled={this.props.video.enabled}
visible={this.props.video.visible}
scope="local" type="video" />
</li>
<li className="conversation-toolbar-btn-box">
<MediaControlButton action={this.handleToggleAudio}
enabled={this.props.audio.enabled}
visible={this.props.audio.visible}
scope="local" type="audio" />
</li>
</ul>
);
/* jshint ignore:end */
}
});
/**
* Conversation view.
*/
var ConversationView = React.createClass({
mixins: [Backbone.Events],
propTypes: {
sdk: React.PropTypes.object.isRequired,
model: React.PropTypes.object.isRequired
video: React.PropTypes.object,
audio: React.PropTypes.object,
initiate: React.PropTypes.bool
},
// height set to 100%" to fix video layout on Google Chrome
@ -149,10 +157,11 @@ loop.shared.views = (function(_, OT, l10n) {
}
},
getInitialProps: function() {
getDefaultProps: function() {
return {
video: {enabled: true},
audio: {enabled: true}
initiate: true,
video: {enabled: true, visible: true},
audio: {enabled: true, visible: true}
};
},
@ -164,26 +173,30 @@ loop.shared.views = (function(_, OT, l10n) {
},
componentWillMount: function() {
this.publisherConfig.publishVideo = this.props.video.enabled;
if (this.props.initiate) {
this.publisherConfig.publishVideo = this.props.video.enabled;
}
},
componentDidMount: function() {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "),
this.stopPublishing);
this.props.model.startSession();
if (this.props.initiate) {
this.listenTo(this.props.model, "session:connected",
this.startPublishing);
this.listenTo(this.props.model, "session:stream-created",
this._streamCreated);
this.listenTo(this.props.model, ["session:peer-hungup",
"session:network-disconnected",
"session:ended"].join(" "),
this.stopPublishing);
this.props.model.startSession();
}
/**
* OT inserts inline styles into the markup. Using a listener for
* resize events helps us trigger a full width/height on the element
* so that they update to the correct dimensions.
* */
* XXX: this should be factored as a mixin.
*/
window.addEventListener('orientationchange', this.updateVideoContainer);
window.addEventListener('resize', this.updateVideoContainer);
},
@ -282,10 +295,12 @@ loop.shared.views = (function(_, OT, l10n) {
* Unpublishes local stream.
*/
stopPublishing: function() {
// Unregister listeners for publisher events
this.stopListening(this.publisher);
if (this.publisher) {
// Unregister listeners for publisher events
this.stopListening(this.publisher);
this.props.model.session.unpublish(this.publisher);
this.props.model.session.unpublish(this.publisher);
}
},
render: function() {
@ -362,7 +377,7 @@ loop.shared.views = (function(_, OT, l10n) {
return {category: "", description: ""};
},
getInitialProps: function() {
getDefaultProps: function() {
return {pending: false};
},
@ -467,8 +482,16 @@ loop.shared.views = (function(_, OT, l10n) {
/**
* 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};
},
@ -488,7 +511,9 @@ loop.shared.views = (function(_, OT, l10n) {
render: function() {
if (this.state.countdown < 1) {
clearInterval(this._timer);
window.close();
if (this.props.onAfterFeedbackReceived) {
this.props.onAfterFeedbackReceived();
}
}
return (
<FeedbackLayout title={l10n.get("feedback_thank_you_heading")}>
@ -509,6 +534,7 @@ loop.shared.views = (function(_, OT, l10n) {
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"])
},
@ -517,7 +543,7 @@ loop.shared.views = (function(_, OT, l10n) {
return {pending: false, step: this.props.step || "start"};
},
getInitialProps: function() {
getDefaultProps: function() {
return {step: "start"};
},
@ -552,7 +578,10 @@ loop.shared.views = (function(_, OT, l10n) {
render: function() {
switch(this.state.step) {
case "finished":
return <FeedbackReceived />;
return (
<FeedbackReceived
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived} />
);
case "form":
return <FeedbackForm feedbackApiClient={this.props.feedbackApiClient}
sendFeedback={this.sendFeedback}

View File

@ -13,6 +13,9 @@
# to the Gruntfile and getting rid of this Makefile entirely.
LOOP_SERVER_URL := $(shell echo $${LOOP_SERVER_URL-http://localhost:5000})
LOOP_FEEDBACK_API_URL := $(shell echo $${LOOP_FEEDBACK_API_URL-"https://input.allizom.org/api/v1/feedback"})
LOOP_FEEDBACK_PRODUCT_NAME := $(shell echo $${LOOP_FEEDBACK_PRODUCT_NAME-Loop})
NODE_LOCAL_BIN=./node_modules/.bin
install: npm_install tos
@ -67,4 +70,6 @@ remove_old_config:
config:
@echo "var loop = loop || {};" > content/config.js
@echo "loop.config = loop.config || {};" >> content/config.js
@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
@echo "loop.config.serverUrl = '`echo $(LOOP_SERVER_URL)`';" >> content/config.js
@echo "loop.config.feedbackApiUrl = '`echo $(LOOP_FEEDBACK_API_URL)`';" >> content/config.js
@echo "loop.config.feedbackProductName = '`echo $(LOOP_FEEDBACK_PRODUCT_NAME)`';" >> content/config.js

View File

@ -29,8 +29,12 @@ appropriate configuration file:
- `LOOP_SERVER_URL` defines the root url of the loop server, without trailing
slash (default: `http://localhost:5000`).
- `LOOP_PENDING_CALL_TIMEOUT` defines the amount of time a pending outgoing call
should be considered timed out, in milliseconds (default: `20000`).
- `LOOP_FEEDBACK_API_URL` sets the root URL for the
[input API](https://input.mozilla.org/); defaults to the input stage server
(https://input.allizom.org/api/v1/feedback). **Don't forget to set this
value to the production server URL when deploying to production.**
- `LOOP_FEEDBACK_PRODUCT_NAME` defines the product name to be sent to the input
API (defaults: Loop).
Usage
-----

View File

@ -207,3 +207,41 @@ body,
flex: 1;
}
/**
* Feedback form overlay (standalone only)
*/
.standalone .ended-conversation {
position: relative;
height: 100%;
background-color: #444;
text-align: left; /* as backup */
text-align: start;
}
.standalone .ended-conversation .feedback {
position: absolute;
width: 50%;
max-width: 400px;
margin: 10px auto;
top: 20px;
left: 10%;
right: 10%;
background: #FFF;
box-shadow: 0px 1px 2px rgba(0, 0, 0, 0.4);
border-radius: 3px;
z-index: 1002; /* ensures the form is always on top of the control bar */
}
.standalone .ended-conversation .local-stream {
/* Hide local media stream when feedback form is shown. */
display: none;
}
@media screen and (max-width:640px) {
.standalone .ended-conversation .feedback {
width: 92%;
top: 10%;
left: 5px;
right: 5px;
}
}

View File

@ -41,6 +41,7 @@
<script type="text/javascript" src="shared/js/models.js"></script>
<script type="text/javascript" src="shared/js/mixins.js"></script>
<script type="text/javascript" src="shared/js/views.js"></script>
<script type="text/javascript" src="shared/js/feedbackApiClient.js"></script>
<script type="text/javascript" src="shared/js/websocket.js"></script>
<script type="text/javascript" src="js/standaloneClient.js"></script>
<script type="text/javascript" src="js/webapp.js"></script>

View File

@ -122,7 +122,7 @@ loop.StandaloneClient = (function($) {
try {
cb(null, this._validate(sessionData, expectedCallsProperties));
} catch (err) {
console.log("Error requesting call info", err);
console.error("Error requesting call info", err.message);
cb(err);
}
}.bind(this));

View File

@ -5,7 +5,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true, React */
/* jshint newcap:false */
/* jshint newcap:false, maxlen:false */
var loop = loop || {};
loop.webapp = (function($, _, OT, mozL10n) {
@ -17,12 +17,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views;
/**
* App router.
* @type {loop.webapp.WebappRouter}
*/
var router;
/**
* Homepage view.
*/
@ -30,7 +24,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
render: function() {
return (
React.DOM.p(null, mozL10n.get("welcome"))
)
);
}
});
@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
render: function() {
/* jshint ignore:start */
return (
React.DOM.div({className: "expired-url-info"},
React.DOM.div({className: "info-panel"},
@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
PromoteFirefoxView({helper: this.props.helper})
)
);
/* jshint ignore:end */
}
});
@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
return (
/* jshint ignore:start */
React.DOM.header({className: "standalone-header header-box container-box"},
ConversationBranding(null),
React.DOM.div({className: "loop-logo", title: "Firefox WebRTC! logo"}),
@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
callUrlCreationDateString
)
)
/* jshint ignore:end */
);
}
});
@ -176,7 +166,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
}
};
},
propTypes: {
@ -200,7 +190,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
render: function() {
var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
return (
/* jshint ignore:start */
React.DOM.div({className: "container"},
React.DOM.div({className: "container-box"},
React.DOM.header({className: "pending-header header-box"},
@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
ConversationFooter(null)
)
/* jshint ignore:end */
);
}
});
@ -237,18 +225,21 @@ loop.webapp = (function($, _, OT, mozL10n) {
/**
* Conversation launcher view. A ConversationModel is associated and attached
* as a `model` property.
*
* Required properties:
* - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*/
var StartConversationView = React.createClass({displayName: 'StartConversationView',
/**
* Constructor.
*
* Required options:
* - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*
*/
propTypes: {
model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
getInitialProps: function() {
getDefaultProps: function() {
return {showCallOptionsMenu: false};
},
@ -260,14 +251,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
};
},
propTypes: {
model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
componentDidMount: function() {
// Listen for events & hide dropdown menu if user clicks away
window.addEventListener("click", this.clickHandler);
@ -348,7 +331,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
return (
/* jshint ignore:start */
React.DOM.div({className: "container"},
React.DOM.div({className: "container-box"},
@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) {
ConversationFooter(null)
)
/* jshint ignore:end */
);
}
});
/**
* Ended conversation view.
*/
var EndedConversationView = React.createClass({displayName: 'EndedConversationView',
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
render: function() {
return (
React.DOM.div({className: "ended-conversation"},
sharedViews.FeedbackView({
feedbackApiClient: this.props.feedbackApiClient,
onAfterFeedbackReceived: this.props.onAfterFeedbackReceived}
),
sharedViews.ConversationView({
initiate: false,
sdk: this.props.sdk,
model: this.props.conversation,
audio: {enabled: false, visible: false},
video: {enabled: false, visible: false}}
)
)
);
}
});
@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired
},
getInitialState: function() {
@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.props.conversation.off(null, null, this);
},
shouldComponentUpdate: function(nextProps, nextState) {
// Only rerender if current state has actually changed
return nextState.callStatus !== this.state.callStatus;
},
callStatusSwitcher: function(status) {
return function() {
this.setState({callStatus: status});
}.bind(this);
},
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "failure":
case "end":
case "start": {
return (
StartConversationView({
@ -472,19 +495,30 @@ loop.webapp = (function($, _, OT, mozL10n) {
case "connected": {
return (
sharedViews.ConversationView({
initiate: true,
sdk: this.props.sdk,
model: this.props.conversation,
video: {enabled: this.props.conversation.hasVideoStream("outgoing")}}
)
);
}
case "end": {
return (
EndedConversationView({
sdk: this.props.sdk,
conversation: this.props.conversation,
feedbackApiClient: this.props.feedbackApiClient,
onAfterFeedbackReceived: this.callStatusSwitcher("start")}
)
);
}
case "expired": {
return (
CallUrlExpiredView({helper: this.props.helper})
);
}
default: {
return HomeView(null)
return HomeView(null);
}
}
},
@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
console.log(error);
console.error(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) {
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
this.setState({callStatus: "end"});
// For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this.props.notifications.errorL10n("call_timeout_notification_text");
}
// redirects the user to the call start view
// XXX should switch callStatus to failed for specific reasons when we
// get the call failed view; for now, switch back to start.
this.setState({callStatus: "start"});
},
/**
@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired
},
getInitialState: function() {
@ -679,7 +716,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
conversation: this.props.conversation,
helper: this.props.helper,
notifications: this.props.notifications,
sdk: this.props.sdk}
sdk: this.props.sdk,
feedbackApiClient: this.props.feedbackApiClient}
)
);
} else {
@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
var feedbackApiClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
user_agent: navigator.userAgent,
url: document.location.origin
});
// Obtain the loopToken and pass it to the conversation
var locationHash = helper.locationHash();
@ -733,7 +777,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
conversation: conversation,
helper: helper,
notifications: notifications,
sdk: OT}
sdk: OT,
feedbackApiClient: feedbackApiClient}
), document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
OutgoingConversationView: OutgoingConversationView,
EndedConversationView: EndedConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,

View File

@ -5,7 +5,7 @@
* You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global loop:true, React */
/* jshint newcap:false */
/* jshint newcap:false, maxlen:false */
var loop = loop || {};
loop.webapp = (function($, _, OT, mozL10n) {
@ -17,12 +17,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views;
/**
* App router.
* @type {loop.webapp.WebappRouter}
*/
var router;
/**
* Homepage view.
*/
@ -30,7 +24,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
render: function() {
return (
<p>{mozL10n.get("welcome")}</p>
)
);
}
});
@ -104,7 +98,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
},
render: function() {
/* jshint ignore:start */
return (
<div className="expired-url-info">
<div className="info-panel">
@ -115,7 +108,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
<PromoteFirefoxView helper={this.props.helper} />
</div>
);
/* jshint ignore:end */
}
});
@ -146,7 +138,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
return (
/* jshint ignore:start */
<header className="standalone-header header-box container-box">
<ConversationBranding />
<div className="loop-logo" title="Firefox WebRTC! logo"></div>
@ -157,7 +148,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
{callUrlCreationDateString}
</h4>
</header>
/* jshint ignore:end */
);
}
});
@ -176,7 +166,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
getInitialState: function() {
return {
callState: this.props.callState || "connecting"
}
};
},
propTypes: {
@ -200,7 +190,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
render: function() {
var callState = mozL10n.get("call_progress_" + this.state.callState + "_description");
return (
/* jshint ignore:start */
<div className="container">
<div className="container-box">
<header className="pending-header header-box">
@ -229,7 +218,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
<ConversationFooter />
</div>
/* jshint ignore:end */
);
}
});
@ -237,18 +225,21 @@ loop.webapp = (function($, _, OT, mozL10n) {
/**
* Conversation launcher view. A ConversationModel is associated and attached
* as a `model` property.
*
* Required properties:
* - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*/
var StartConversationView = React.createClass({
/**
* Constructor.
*
* Required options:
* - {loop.shared.models.ConversationModel} model Conversation model.
* - {loop.shared.models.NotificationCollection} notifications
*
*/
propTypes: {
model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
getInitialProps: function() {
getDefaultProps: function() {
return {showCallOptionsMenu: false};
},
@ -260,14 +251,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
};
},
propTypes: {
model: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
// XXX Check more tightly here when we start injecting window.loop.*
notifications: React.PropTypes.object.isRequired,
client: React.PropTypes.object.isRequired
},
componentDidMount: function() {
// Listen for events & hide dropdown menu if user clicks away
window.addEventListener("click", this.clickHandler);
@ -348,7 +331,6 @@ loop.webapp = (function($, _, OT, mozL10n) {
});
return (
/* jshint ignore:start */
<div className="container">
<div className="container-box">
@ -407,7 +389,37 @@ loop.webapp = (function($, _, OT, mozL10n) {
<ConversationFooter />
</div>
/* jshint ignore:end */
);
}
});
/**
* Ended conversation view.
*/
var EndedConversationView = React.createClass({
propTypes: {
conversation: React.PropTypes.instanceOf(sharedModels.ConversationModel)
.isRequired,
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired,
onAfterFeedbackReceived: React.PropTypes.func.isRequired
},
render: function() {
return (
<div className="ended-conversation">
<sharedViews.FeedbackView
feedbackApiClient={this.props.feedbackApiClient}
onAfterFeedbackReceived={this.props.onAfterFeedbackReceived}
/>
<sharedViews.ConversationView
initiate={false}
sdk={this.props.sdk}
model={this.props.conversation}
audio={{enabled: false, visible: false}}
video={{enabled: false, visible: false}}
/>
</div>
);
}
});
@ -426,7 +438,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired
},
getInitialState: function() {
@ -450,13 +463,23 @@ loop.webapp = (function($, _, OT, mozL10n) {
this.props.conversation.off(null, null, this);
},
shouldComponentUpdate: function(nextProps, nextState) {
// Only rerender if current state has actually changed
return nextState.callStatus !== this.state.callStatus;
},
callStatusSwitcher: function(status) {
return function() {
this.setState({callStatus: status});
}.bind(this);
},
/**
* Renders the conversation views.
*/
render: function() {
switch (this.state.callStatus) {
case "failure":
case "end":
case "start": {
return (
<StartConversationView
@ -472,19 +495,30 @@ loop.webapp = (function($, _, OT, mozL10n) {
case "connected": {
return (
<sharedViews.ConversationView
initiate={true}
sdk={this.props.sdk}
model={this.props.conversation}
video={{enabled: this.props.conversation.hasVideoStream("outgoing")}}
/>
);
}
case "end": {
return (
<EndedConversationView
sdk={this.props.sdk}
conversation={this.props.conversation}
feedbackApiClient={this.props.feedbackApiClient}
onAfterFeedbackReceived={this.callStatusSwitcher("start")}
/>
);
}
case "expired": {
return (
<CallUrlExpiredView helper={this.props.helper} />
);
}
default: {
return <HomeView />
return <HomeView />;
}
}
},
@ -494,7 +528,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
* @param {{code: number, message: string}} error
*/
_notifyError: function(error) {
console.log(error);
console.error(error);
this.props.notifications.errorL10n("connection_error_see_console_notification");
this.setState({callStatus: "end"});
},
@ -628,13 +662,15 @@ loop.webapp = (function($, _, OT, mozL10n) {
* @param {String} reason The reason the call was terminated.
*/
_handleCallTerminated: function(reason) {
this.setState({callStatus: "end"});
// For reasons other than cancel, display some notification text.
if (reason !== "cancel") {
// XXX This should really display the call failed view - bug 1046959
// will implement this.
this.props.notifications.errorL10n("call_timeout_notification_text");
}
// redirects the user to the call start view
// XXX should switch callStatus to failed for specific reasons when we
// get the call failed view; for now, switch back to start.
this.setState({callStatus: "start"});
},
/**
@ -657,7 +693,8 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper: React.PropTypes.instanceOf(WebappHelper).isRequired,
notifications: React.PropTypes.instanceOf(sharedModels.NotificationCollection)
.isRequired,
sdk: React.PropTypes.object.isRequired
sdk: React.PropTypes.object.isRequired,
feedbackApiClient: React.PropTypes.object.isRequired
},
getInitialState: function() {
@ -680,6 +717,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper={this.props.helper}
notifications={this.props.notifications}
sdk={this.props.sdk}
feedbackApiClient={this.props.feedbackApiClient}
/>
);
} else {
@ -721,6 +759,12 @@ loop.webapp = (function($, _, OT, mozL10n) {
var conversation = new sharedModels.ConversationModel({}, {
sdk: OT
});
var feedbackApiClient = new loop.FeedbackAPIClient(
loop.config.feedbackApiUrl, {
product: loop.config.feedbackProductName,
user_agent: navigator.userAgent,
url: document.location.origin
});
// Obtain the loopToken and pass it to the conversation
var locationHash = helper.locationHash();
@ -734,6 +778,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
helper={helper}
notifications={notifications}
sdk={OT}
feedbackApiClient={feedbackApiClient}
/>, document.querySelector("#main"));
// Set the 'lang' and 'dir' attributes to <html> when the page is translated
@ -746,6 +791,7 @@ loop.webapp = (function($, _, OT, mozL10n) {
PendingConversationView: PendingConversationView,
StartConversationView: StartConversationView,
OutgoingConversationView: OutgoingConversationView,
EndedConversationView: EndedConversationView,
HomeView: HomeView,
UnsupportedBrowserView: UnsupportedBrowserView,
UnsupportedDeviceView: UnsupportedDeviceView,

View File

@ -46,3 +46,32 @@ clientShortname=WebRTC!
call_url_creation_date_label=(from {{call_url_creation_date}})
call_progress_connecting_description=Connecting…
call_progress_ringing_description=Ringing…
feedback_call_experience_heading2=How was your conversation?
feedback_what_makes_you_sad=What makes you sad?
feedback_thank_you_heading=Thank you for your feedback!
feedback_category_audio_quality=Audio quality
feedback_category_video_quality=Video quality
feedback_category_was_disconnected=Was disconnected
feedback_category_confusing=Confusing
feedback_category_other=Other:
feedback_custom_category_text_placeholder=What went wrong?
feedback_submit_button=Submit
feedback_back_button=Back
## LOCALIZATION NOTE (feedback_window_will_close_in2):
## Gaia l10n format; see https://github.com/mozilla-b2g/gaia/blob/f108c706fae43cd61628babdd9463e7695b2496e/apps/email/locales/email.en-US.properties#L387
## In this item, don't translate the part between {{..}}
feedback_window_will_close_in2={[ plural(countdown) ]}
feedback_window_will_close_in2[one] = This window will close in {{countdown}} second
feedback_window_will_close_in2[two] = This window will close in {{countdown}} seconds
feedback_window_will_close_in2[few] = This window will close in {{countdown}} seconds
feedback_window_will_close_in2[many] = This window will close in {{countdown}} seconds
feedback_window_will_close_in2[other] = This window will close in {{countdown}} seconds
## LOCALIZATION_NOTE (feedback_rejoin_button): Displayed on the feedback form after
## a signed-in to signed-in user call.
## https://people.mozilla.org/~dhenein/labs/loop-mvp-spec/#feedback
feedback_rejoin_button=Rejoin
## LOCALIZATION NOTE (feedback_report_user_button): Used to report a user in the case of
## an abusive user.
feedback_report_user_button=Report User

View File

@ -7,17 +7,21 @@ var app = express();
var port = process.env.PORT || 3000;
var loopServerPort = process.env.LOOP_SERVER_PORT || 5000;
var feedbackApiUrl = process.env.LOOP_FEEDBACK_API_URL ||
"https://input.allizom.org/api/v1/feedback";
var feedbackProductName = process.env.LOOP_FEEDBACK_PRODUCT_NAME || "Loop";
function getConfigFile(req, res) {
"use strict";
res.set('Content-Type', 'text/javascript');
res.send(
"var loop = loop || {};" +
"loop.config = loop.config || {};" +
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';" +
"loop.config.pendingCallTimeout = 20000;"
);
res.send([
"var loop = loop || {};",
"loop.config = loop.config || {};",
"loop.config.serverUrl = 'http://localhost:" + loopServerPort + "';",
"loop.config.feedbackApiUrl = '" + feedbackApiUrl + "';",
"loop.config.feedbackProductName = '" + feedbackProductName + "';",
].join("\n"));
}
app.get('/content/config.js', getConfigFile);

View File

@ -108,8 +108,7 @@ describe("loop.conversation", function() {
beforeEach(function() {
client = new loop.Client();
conversation = new loop.shared.models.ConversationModel({}, {
sdk: {},
pendingCallTimeout: 1000,
sdk: {}
});
sandbox.spy(conversation, "setIncomingSessionData");
sandbox.stub(conversation, "setOutgoingSessionData");

View File

@ -138,6 +138,13 @@ describe("loop.FeedbackAPIClient", function() {
expect(parsed.user_agent).eql("MOZAGENT");
});
it("should send url information when provided", function() {
client.send({url: "http://fake.invalid"}, function(){});
var parsed = JSON.parse(requests[0].requestBody);
expect(parsed.url).eql("http://fake.invalid");
});
it("should throw on invalid feedback data", function() {
expect(function() {
client.send("invalid data", function(){});

View File

@ -60,8 +60,7 @@ describe("loop.shared.router", function() {
conversation = new loop.shared.models.ConversationModel({
loopToken: "fakeToken"
}, {
sdk: {},
pendingCallTimeout: 1000
sdk: {}
});
});

View File

@ -187,13 +187,12 @@ describe("loop.shared.views", function() {
initSession: sandbox.stub().returns(fakeSession)
};
model = new sharedModels.ConversationModel(fakeSessionData, {
sdk: fakeSDK,
pendingCallTimeout: 1000
sdk: fakeSDK
});
});
describe("#componentDidMount", function() {
it("should start a session", function() {
it("should start a session by default", function() {
sandbox.stub(model, "startSession");
mountTestComponent({
@ -205,6 +204,19 @@ describe("loop.shared.views", function() {
sinon.assert.calledOnce(model.startSession);
});
it("shouldn't start a session if initiate is false", function() {
sandbox.stub(model, "startSession");
mountTestComponent({
initiate: false,
sdk: fakeSDK,
model: model,
video: {enabled: true}
});
sinon.assert.notCalled(model.startSession);
});
it("should set the correct stream publish options", function() {
var component = mountTestComponent({

View File

@ -36,6 +36,7 @@
<script src="../../content/shared/js/mixins.js"></script>
<script src="../../content/shared/js/views.js"></script>
<script src="../../content/shared/js/websocket.js"></script>
<script src="../../content/shared/js/feedbackApiClient.js"></script>
<script src="../../standalone/content/js/standaloneClient.js"></script>
<script src="../../standalone/content/js/webapp.js"></script>
<!-- Test scripts -->

View File

@ -13,11 +13,15 @@ describe("loop.webapp", function() {
var sharedModels = loop.shared.models,
sharedViews = loop.shared.views,
sandbox,
notifications;
notifications,
feedbackApiClient;
beforeEach(function() {
sandbox = sinon.sandbox.create();
notifications = new sharedModels.NotificationCollection();
feedbackApiClient = new loop.FeedbackAPIClient("http://invalid", {
product: "Loop"
});
});
afterEach(function() {
@ -31,6 +35,7 @@ describe("loop.webapp", function() {
sandbox.stub(React, "renderComponent");
sandbox.stub(loop.webapp.WebappHelper.prototype,
"locationHash").returns("#call/fake-Token");
loop.config.feedbackApiUrl = "http://fake.invalid";
conversationSetStub =
sandbox.stub(sharedModels.ConversationModel.prototype, "set");
});
@ -77,7 +82,8 @@ describe("loop.webapp", function() {
client: client,
conversation: conversation,
notifications: notifications,
sdk: {}
sdk: {},
feedbackApiClient: feedbackApiClient
});
});
@ -305,7 +311,7 @@ describe("loop.webapp", function() {
conversation.trigger("session:ended");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
loop.webapp.EndedConversationView);
});
});
@ -314,7 +320,7 @@ describe("loop.webapp", function() {
conversation.trigger("session:peer-hungup");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
loop.webapp.EndedConversationView);
});
it("should notify the user", function() {
@ -333,7 +339,7 @@ describe("loop.webapp", function() {
conversation.trigger("session:network-disconnected");
TestUtils.findRenderedComponentWithType(ocView,
loop.webapp.StartConversationView);
loop.webapp.EndedConversationView);
});
it("should notify the user", function() {
@ -474,8 +480,10 @@ describe("loop.webapp", function() {
loop.webapp.WebappRootView({
client: client,
helper: webappHelper,
notifications: notifications,
sdk: sdk,
conversation: conversationModel
conversation: conversationModel,
feedbackApiClient: feedbackApiClient
}));
}
@ -772,6 +780,32 @@ describe("loop.webapp", function() {
});
});
describe("EndedConversationView", function() {
var view, conversation;
beforeEach(function() {
conversation = new sharedModels.ConversationModel({}, {
sdk: {}
});
view = React.addons.TestUtils.renderIntoDocument(
loop.webapp.EndedConversationView({
conversation: conversation,
sdk: {},
feedbackApiClient: feedbackApiClient,
onAfterFeedbackReceived: function(){}
})
);
});
it("should render a ConversationView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.ConversationView);
});
it("should render a FeedbackView", function() {
TestUtils.findRenderedComponentWithType(view, sharedViews.FeedbackView);
});
});
describe("PromoteFirefoxView", function() {
describe("#render", function() {
it("should not render when using Firefox", function() {

View File

@ -9,6 +9,10 @@
* @type {Object}
*/
navigator.mozL10n = document.mozL10n = {
initialize: function(){},
getDirection: function(){},
get: function(stringId, vars) {
// upcase the first letter

View File

@ -7,6 +7,7 @@
* @type {Object}
*/
navigator.mozLoop = {
ensureRegistered: function() {},
getLoopCharPref: function() {},
getLoopBoolPref: function() {}
};

View File

@ -37,7 +37,7 @@
.showcase > section {
position: relative;
padding-top: 12em;
padding-top: 14em;
clear: both;
}
@ -149,3 +149,9 @@
* When tokbox inserts the markup into the page the problem goes away */
bottom: auto;
}
.standalone .ended-conversation .remote_wrapper,
.standalone .video-layout-wrapper {
/* Removes the fake video image for ended conversations */
background: none;
}

View File

@ -23,6 +23,7 @@
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
@ -338,6 +339,19 @@
)
),
Section({name: "EndedConversationView"},
Example({summary: "Displays the feedback form"},
React.DOM.div({className: "standalone"},
EndedConversationView({sdk: mockSDK,
video: {enabled: true},
audio: {enabled: true},
conversation: mockConversationModel,
feedbackApiClient: stageFeedbackApiClient,
onAfterFeedbackReceived: noop})
)
)
),
Section({name: "AlertMessages"},
Example({summary: "Various alerts"},
React.DOM.div({className: "alert alert-warning"},

View File

@ -23,6 +23,7 @@
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var PendingConversationView = loop.webapp.PendingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
@ -338,6 +339,19 @@
</Example>
</Section>
<Section name="EndedConversationView">
<Example summary="Displays the feedback form">
<div className="standalone">
<EndedConversationView sdk={mockSDK}
video={{enabled: true}}
audio={{enabled: true}}
conversation={mockConversationModel}
feedbackApiClient={stageFeedbackApiClient}
onAfterFeedbackReceived={noop} />
</div>
</Example>
</Section>
<Section name="AlertMessages">
<Example summary="Various alerts">
<div className="alert alert-warning">

View File

@ -75,6 +75,8 @@ let UI = {
}
Services.prefs.setBoolPref("devtools.webide.autoinstallADBHelper", false);
this.lastConnectedRuntime = Services.prefs.getCharPref("devtools.webide.lastConnectedRuntime");
this.setupDeck();
},
@ -125,6 +127,7 @@ let UI = {
switch (what) {
case "runtimelist":
this.updateRuntimeList();
this.autoConnectRuntime();
break;
case "connection":
this.updateRuntimeButton();
@ -145,6 +148,7 @@ let UI = {
break;
case "runtime":
this.updateRuntimeButton();
this.saveLastConnectedRuntime();
break;
case "project-validated":
this.updateTitle();
@ -343,6 +347,34 @@ let UI = {
}
},
autoConnectRuntime: function () {
// Automatically reconnect to the previously selected runtime,
// if available and has an ID
if (AppManager.selectedRuntime || !this.lastConnectedRuntime) {
return;
}
let [_, type, id] = this.lastConnectedRuntime.match(/^(\w+):(.+)$/);
type = type.toLowerCase();
// Local connection is mapped to AppManager.runtimeList.custom array
if (type == "local") {
type = "custom";
}
// We support most runtimes except simulator, that needs to be manually
// launched
if (type == "usb" || type == "wifi" || type == "custom") {
for (let runtime of AppManager.runtimeList[type]) {
// Some runtimes do not expose getID function and don't support
// autoconnect (like remote connection)
if (typeof(runtime.getID) == "function" && runtime.getID() == id) {
this.connectToRuntime(runtime);
}
}
}
},
connectToRuntime: function(runtime) {
let name = runtime.getName();
let promise = AppManager.connectToRuntime(runtime);
@ -359,6 +391,17 @@ let UI = {
}
},
saveLastConnectedRuntime: function () {
if (AppManager.selectedRuntime &&
typeof(AppManager.selectedRuntime.getID) === "function") {
this.lastConnectedRuntime = AppManager.selectedRuntime.type + ":" + AppManager.selectedRuntime.getID();
} else {
this.lastConnectedRuntime = "";
}
Services.prefs.setCharPref("devtools.webide.lastConnectedRuntime",
this.lastConnectedRuntime);
},
/********** PROJECTS **********/
// Panel & button

View File

@ -651,7 +651,15 @@ exports.AppManager = AppManager = {
let r = new USBRuntime(id);
this.runtimeList.usb.push(r);
r.updateNameFromADB().then(
() => this.update("runtimelist"), () => {});
() => {
this.update("runtimelist");
// Also update the runtime button label, if the currently selected
// runtime name changes
if (r == this.selectedRuntime) {
this.update("runtime");
}
},
() => {});
}
this.update("runtimelist");
},

View File

@ -13,11 +13,21 @@ const promise = require("promise");
const Strings = Services.strings.createBundle("chrome://browser/locale/devtools/webide.properties");
// These type strings are used for logging events to Telemetry
let RuntimeTypes = {
usb: "USB",
wifi: "WIFI",
simulator: "SIMULATOR",
remote: "REMOTE",
local: "LOCAL"
};
function USBRuntime(id) {
this.id = id;
}
USBRuntime.prototype = {
type: RuntimeTypes.usb,
connect: function(connection) {
let device = Devices.getByName(this.id);
if (!device) {
@ -59,6 +69,7 @@ function WiFiRuntime(deviceName) {
}
WiFiRuntime.prototype = {
type: RuntimeTypes.wifi,
connect: function(connection) {
let service = discovery.getRemoteService("devtools", this.deviceName);
if (!service) {
@ -82,6 +93,7 @@ function SimulatorRuntime(version) {
}
SimulatorRuntime.prototype = {
type: RuntimeTypes.simulator,
connect: function(connection) {
let port = ConnectionManager.getFreeTCPPort();
let simulator = Simulator.getByVersion(this.version);
@ -105,6 +117,7 @@ SimulatorRuntime.prototype = {
}
let gLocalRuntime = {
type: RuntimeTypes.local,
connect: function(connection) {
if (!DebuggerServer.initialized) {
DebuggerServer.init();
@ -118,9 +131,13 @@ let gLocalRuntime = {
getName: function() {
return Strings.GetStringFromName("local_runtime");
},
getID: function () {
return "local";
}
}
let gRemoteRuntime = {
type: RuntimeTypes.remote,
connect: function(connection) {
let win = Services.wm.getMostRecentWindow("devtools:webide");
if (!win) {

View File

@ -31,3 +31,4 @@ support-files =
[test_manifestUpdate.html]
[test_addons.html]
[test_deviceinfo.html]
[test_autoconnect_runtime.html]

View File

@ -0,0 +1,91 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf8">
<title></title>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/chrome-harness.js"></script>
<script type="application/javascript;version=1.8" src="head.js"></script>
<link rel="stylesheet" type="text/css" href="chrome://mochikit/content/tests/SimpleTest/test.css">
</head>
<body>
<script type="application/javascript;version=1.8">
window.onload = function() {
SimpleTest.waitForExplicitFinish();
Task.spawn(function* () {
Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
DebuggerServer.init(function () { return true; });
DebuggerServer.addBrowserActors();
let win = yield openWebIDE();
let fakeRuntime = {
type: "USB",
connect: function(connection) {
ok(connection, win.AppManager.connection, "connection is valid");
connection.host = null; // force connectPipe
connection.connect();
return promise.resolve();
},
getID: function() {
return "fakeRuntime";
},
getName: function() {
return "fakeRuntime";
}
};
win.AppManager.runtimeList.usb.push(fakeRuntime);
win.AppManager.update("runtimelist");
let panelNode = win.document.querySelector("#runtime-panel");
let items = panelNode.querySelectorAll(".runtime-panel-item-usb");
is(items.length, 1, "Found one runtime button");
let deferred = promise.defer();
win.AppManager.connection.once(
win.Connection.Events.CONNECTED,
() => deferred.resolve());
items[0].click();
ok(win.document.querySelector("window").className, "busy", "UI is busy");
yield win.UI._busyPromise;
is(Object.keys(DebuggerServer._connections).length, 1, "Connected");
yield nextTick();
yield closeWebIDE(win);
is(Object.keys(DebuggerServer._connections).length, 0, "Disconnected");
win = yield openWebIDE();
win.AppManager.runtimeList.usb.push(fakeRuntime);
win.AppManager.update("runtimelist");
yield waitForUpdate(win, "list-tabs-response");
is(Object.keys(DebuggerServer._connections).length, 1, "Automatically reconnected");
yield win.Cmds.disconnectRuntime();
yield closeWebIDE(win);
DebuggerServer.destroy();
SimpleTest.finish();
});
}
</script>
</body>
</html>

View File

@ -15,3 +15,4 @@ pref("devtools.webide.simulatorAddonID", "fxos_#SLASHED_VERSION#_simulator@mozil
pref("devtools.webide.adbAddonURL", "https://ftp.mozilla.org/pub/mozilla.org/labs/fxos-simulator/adb-helper/#OS#/adbhelper-#OS#-latest.xpi");
pref("devtools.webide.adbAddonID", "adbhelper@mozilla.org");
pref("devtools.webide.monitorWebSocketURL", "ws://localhost:9000");
pref("devtools.webide.lastConnectedRuntime", "");

View File

@ -105,9 +105,9 @@ g.edgePath.param-connection {
fill: #4c9ed9; /* Select Highlight Blue */
}
/* Text in nodes */
/* Text in nodes and edges */
text {
cursor: pointer;
cursor: default; /* override the "text" cursor */
font-weight: 300;
font-family: "Helvetica Neue", Helvetica, Arial, sans-serf;
font-size: 14px;
@ -123,6 +123,10 @@ text {
fill: #f0f1f2; /* Toolbars */
}
.nodes text {
cursor: pointer;
}
/**
* Inspector Styles
*/

View File

@ -283,6 +283,13 @@ pref("browser.search.official", true);
// Control media casting feature
pref("browser.casting.enabled", true);
#ifdef RELEASE_BUILD
pref("browser.mirroring.enabled", false);
pref("browser.mirroring.enabled.roku", false);
#else
pref("browser.mirroring.enabled", true);
pref("browser.mirroring.enabled.roku", true);
#endif
// Enable sparse localization by setting a few package locale overrides
pref("chrome.override_package.global", "browser");

View File

@ -26,7 +26,7 @@
</RelativeLayout>
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsListContainer"
<view class="org.mozilla.gecko.tabs.TabsPanel$PanelViewContainer"
android:id="@+id/tabs_container"
android:layout_width="match_parent"
android:layout_height="0dip"

View File

@ -26,7 +26,7 @@
</RelativeLayout>
<view class="org.mozilla.gecko.tabs.TabsPanel$TabsListContainer"
<view class="org.mozilla.gecko.tabs.TabsPanel$PanelViewContainer"
android:id="@+id/tabs_container"
android:layout_width="match_parent"
android:layout_height="wrap_content">

View File

@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<item type="layout" name="tabs_row">@layout/tabs_item_cell</item>
<item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
</resources>

View File

@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<item type="layout" name="tabs_row">@layout/tabs_item_cell</item>
<item type="layout" name="tabs_layout_item_view">@layout/tabs_item_cell</item>
</resources>

View File

@ -4,5 +4,5 @@
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<resources>
<item type="layout" name="tabs_row">@layout/tabs_item_row</item>
<item type="layout" name="tabs_layout_item_view">@layout/tabs_item_row</item>
</resources>

View File

@ -81,7 +81,7 @@ public class TabsLayoutAdapter extends BaseAdapter {
}
View newView(int position, ViewGroup parent) {
final View view = mInflater.inflate(R.layout.tabs_row, parent, false);
final View view = mInflater.inflate(R.layout.tabs_layout_item_view, parent, false);
final TabsLayoutItemView item = new TabsLayoutItemView(view);
view.setTag(item);
return view;

View File

@ -77,7 +77,7 @@ public class TabsPanel extends LinearLayout
private final GeckoApp mActivity;
private final LightweightTheme mTheme;
private RelativeLayout mHeader;
private TabsListContainer mTabsContainer;
private PanelViewContainer mPanelsContainer;
private PanelView mPanel;
private PanelView mPanelNormal;
private PanelView mPanelPrivate;
@ -137,7 +137,7 @@ public class TabsPanel extends LinearLayout
private void initialize() {
mHeader = (RelativeLayout) findViewById(R.id.tabs_panel_header);
mTabsContainer = (TabsListContainer) findViewById(R.id.tabs_container);
mPanelsContainer = (PanelViewContainer) findViewById(R.id.tabs_container);
mPanelNormal = (PanelView) findViewById(R.id.normal_tabs);
mPanelNormal.setTabsPanel(this);
@ -264,10 +264,10 @@ public class TabsPanel extends LinearLayout
return mActivity.onOptionsItemSelected(item);
}
private static int getTabContainerHeight(TabsListContainer listContainer) {
Resources resources = listContainer.getContext().getResources();
private static int getPanelsContainerHeight(PanelViewContainer panelsContainer) {
Resources resources = panelsContainer.getContext().getResources();
PanelView panelView = listContainer.getCurrentPanelView();
PanelView panelView = panelsContainer.getCurrentPanelView();
if (panelView != null && !panelView.shouldExpand()) {
return resources.getDimensionPixelSize(R.dimen.tabs_tray_horizontal_height);
}
@ -276,7 +276,7 @@ public class TabsPanel extends LinearLayout
int screenHeight = resources.getDisplayMetrics().heightPixels;
Rect windowRect = new Rect();
listContainer.getWindowVisibleDisplayFrame(windowRect);
panelsContainer.getWindowVisibleDisplayFrame(windowRect);
int windowHeight = windowRect.bottom - windowRect.top;
// The web content area should have at least 1.5x the height of the action bar.
@ -323,9 +323,9 @@ public class TabsPanel extends LinearLayout
onLightweightThemeChanged();
}
// Tabs List Container holds the ListView
static class TabsListContainer extends FrameLayout {
public TabsListContainer(Context context, AttributeSet attrs) {
// Panel View Container holds the ListView
static class PanelViewContainer extends FrameLayout {
public PanelViewContainer(Context context, AttributeSet attrs) {
super(context, attrs);
}
@ -346,7 +346,7 @@ public class TabsPanel extends LinearLayout
@Override
protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) {
if (!GeckoAppShell.getGeckoInterface().hasTabsSideBar()) {
int heightSpec = MeasureSpec.makeMeasureSpec(getTabContainerHeight(TabsListContainer.this), MeasureSpec.EXACTLY);
int heightSpec = MeasureSpec.makeMeasureSpec(getPanelsContainerHeight(PanelViewContainer.this), MeasureSpec.EXACTLY);
super.onMeasure(widthMeasureSpec, heightSpec);
} else {
super.onMeasure(widthMeasureSpec, heightMeasureSpec);
@ -468,7 +468,7 @@ public class TabsPanel extends LinearLayout
dispatchLayoutChange(getWidth(), getHeight());
} else {
int actionBarHeight = mContext.getResources().getDimensionPixelSize(R.dimen.browser_toolbar_height);
int height = actionBarHeight + getTabContainerHeight(mTabsContainer);
int height = actionBarHeight + getPanelsContainerHeight(mPanelsContainer);
dispatchLayoutChange(getWidth(), height);
}
mHeaderVisible = true;
@ -526,13 +526,13 @@ public class TabsPanel extends LinearLayout
final int tabsPanelWidth = getWidth();
if (mVisible) {
ViewHelper.setTranslationX(mHeader, -tabsPanelWidth);
ViewHelper.setTranslationX(mTabsContainer, -tabsPanelWidth);
ViewHelper.setTranslationX(mPanelsContainer, -tabsPanelWidth);
// The footer view is only present on the sidebar, v11+.
ViewHelper.setTranslationX(mFooter, -tabsPanelWidth);
}
final int translationX = (mVisible ? 0 : -tabsPanelWidth);
animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_X, translationX);
animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_X, translationX);
animator.attach(mFooter, PropertyAnimator.Property.TRANSLATION_X, translationX);
@ -542,16 +542,16 @@ public class TabsPanel extends LinearLayout
final int translationY = (mVisible ? 0 : -toolbarHeight);
if (mVisible) {
ViewHelper.setTranslationY(mHeader, -toolbarHeight);
ViewHelper.setTranslationY(mTabsContainer, -toolbarHeight);
ViewHelper.setAlpha(mTabsContainer, 0.0f);
ViewHelper.setTranslationY(mPanelsContainer, -toolbarHeight);
ViewHelper.setAlpha(mPanelsContainer, 0.0f);
}
animator.attach(mTabsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
animator.attach(mTabsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
animator.attach(mPanelsContainer, PropertyAnimator.Property.ALPHA, mVisible ? 1.0f : 0.0f);
animator.attach(mPanelsContainer, PropertyAnimator.Property.TRANSLATION_Y, translationY);
animator.attach(mHeader, PropertyAnimator.Property.TRANSLATION_Y, translationY);
}
mHeader.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mTabsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
mPanelsContainer.setLayerType(View.LAYER_TYPE_HARDWARE, null);
}
public void finishTabsAnimation() {
@ -560,7 +560,7 @@ public class TabsPanel extends LinearLayout
}
mHeader.setLayerType(View.LAYER_TYPE_NONE, null);
mTabsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
mPanelsContainer.setLayerType(View.LAYER_TYPE_NONE, null);
// If the tray is now hidden, call hide() on current panel and unset it as the current panel
// to avoid hide() being called again when the tray is opened next.

View File

@ -16,6 +16,7 @@ var rokuDevice = {
Cu.import("resource://gre/modules/RokuApp.jsm");
return new RokuApp(aService);
},
mirror: Services.prefs.getBoolPref("browser.mirroring.enabled.roku"),
types: ["video/mp4"],
extensions: ["mp4"]
};
@ -52,7 +53,7 @@ var CastingApps = {
mirrorStopMenuId: -1,
init: function ca_init() {
if (!this.isEnabled()) {
if (!this.isCastingEnabled()) {
return;
}
@ -99,22 +100,25 @@ var CastingApps = {
NativeWindow.contextmenus.remove(this._castMenuId);
},
_mirrorStarted: function(stopMirrorCallback) {
this.stopMirrorCallback = stopMirrorCallback;
NativeWindow.menu.update(this.mirrorStartMenuId, { visible: false });
NativeWindow.menu.update(this.mirrorStopMenuId, { visible: true });
},
serviceAdded: function(aService) {
if (aService.mirror && this.mirrorStartMenuId == -1) {
if (this.isMirroringEnabled() && aService.mirror && this.mirrorStartMenuId == -1) {
this.mirrorStartMenuId = NativeWindow.menu.add({
name: Strings.browser.GetStringFromName("casting.mirrorTab"),
callback: function() {
function callbackFunc(aService) {
let callbackFunc = function(aService) {
let app = SimpleServiceDiscovery.findAppForService(aService);
if (app)
app.mirror(function() {
});
}
if (app) {
app.mirror(function() {}, window, BrowserApp.selectedTab.getViewport(), this._mirrorStarted.bind(this));
}
}.bind(this);
function filterFunc(aService) {
return aService.mirror == true;
}
this.prompt(callbackFunc, filterFunc);
this.prompt(callbackFunc, aService => aService.mirror);
}.bind(this),
parent: NativeWindow.menu.toolsMenuID
});
@ -125,6 +129,9 @@ var CastingApps = {
if (this.tabMirror) {
this.tabMirror.stop();
this.tabMirror = null;
} else if (this.stopMirrorCallback) {
this.stopMirrorCallback();
this.stopMirrorCallback = null;
}
NativeWindow.menu.update(this.mirrorStartMenuId, { visible: true });
NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
@ -132,7 +139,9 @@ var CastingApps = {
parent: NativeWindow.menu.toolsMenuID
});
}
NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
if (this.mirrorStartMenuId != -1) {
NativeWindow.menu.update(this.mirrorStopMenuId, { visible: false });
}
},
serviceLost: function(aService) {
@ -150,10 +159,14 @@ var CastingApps = {
}
},
isEnabled: function isEnabled() {
isCastingEnabled: function isCastingEnabled() {
return Services.prefs.getBoolPref("browser.casting.enabled");
},
isMirroringEnabled: function isMirroringEnabled() {
return Services.prefs.getBoolPref("browser.mirroring.enabled");
},
observe: function (aSubject, aTopic, aData) {
switch (aTopic) {
case "Casting:Play":

View File

@ -4,14 +4,9 @@
"use strict";
XPCOMUtils.defineLazyModuleGetter(this, "Notifications", "resource://gre/modules/Notifications.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "contentPrefs",
"@mozilla.org/content-pref/service;1",
"nsIContentPrefService2");
var WebrtcUI = {
_notificationId: null,
VIDEO_SOURCE: "videoSource",
AUDIO_SOURCE: "audioDevice",
observe: function(aSubject, aTopic, aData) {
if (aTopic === "getUserMedia:request") {
@ -83,42 +78,39 @@ var WebrtcUI = {
contentWindow.navigator.mozGetUserMediaDevices(
constraints,
function (aDevices) {
WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio, constraints.video, aDevices);
function (devices) {
WebrtcUI.prompt(contentWindow, aSubject.callID, constraints.audio,
constraints.video, devices);
},
Cu.reportError, aSubject.innerWindowID);
function (error) {
Cu.reportError(error);
},
aSubject.innerWindowID);
},
getDeviceButtons: function(aAudioDevices, aVideoDevices, aCallID, aHost) {
getDeviceButtons: function(audioDevices, videoDevices, aCallID) {
return [{
label: Strings.browser.GetStringFromName("getUserMedia.denyRequest.label"),
callback: () => {
callback: function() {
Services.obs.notifyObservers(null, "getUserMedia:response:deny", aCallID);
}
}, {
},
{
label: Strings.browser.GetStringFromName("getUserMedia.shareRequest.label"),
callback: (checked /* ignored */, inputs) => {
callback: function(checked /* ignored */, inputs) {
let allowedDevices = Cc["@mozilla.org/supports-array;1"].createInstance(Ci.nsISupportsArray);
let audioId = 0;
if (inputs && inputs[this.AUDIO_SOURCE] != undefined) {
audioId = inputs[this.AUDIO_SOURCE];
}
if (aAudioDevices[audioId]) {
allowedDevices.AppendElement(aAudioDevices[audioId]);
this.setDefaultDevice(this.AUDIO_SOURCE, aAudioDevices[audioId].name, aHost);
}
if (inputs && inputs.audioDevice != undefined)
audioId = inputs.audioDevice;
if (audioDevices[audioId])
allowedDevices.AppendElement(audioDevices[audioId]);
let videoId = 0;
if (inputs && inputs[this.VIDEO_SOURCE] != undefined) {
videoId = inputs[this.VIDEO_SOURCE];
}
if (aVideoDevices[videoId]) {
allowedDevices.AppendElement(aVideoDevices[videoId]);
this.setDefaultDevice(this.VIDEO_SOURCE, aVideoDevices[videoId].name, aHost);
}
if (inputs && inputs.videoSource != undefined)
videoId = inputs.videoSource;
if (videoDevices[videoId])
allowedDevices.AppendElement(videoDevices[videoId]);
Services.obs.notifyObservers(allowedDevices, "getUserMedia:response:allow", aCallID);
}
@ -129,34 +121,30 @@ var WebrtcUI = {
_getList: function(aDevices, aType) {
let defaultCount = 0;
return aDevices.map(function(device) {
let name = device.name;
// if this is a Camera input, convert the name to something readable
let res = /Camera\ \d+,\ Facing (front|back)/.exec(name);
if (res) {
return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
}
// if this is a Camera input, convert the name to something readable
let res = /Camera\ \d+,\ Facing (front|back)/.exec(device.name);
if (res)
return Strings.browser.GetStringFromName("getUserMedia." + aType + "." + res[1] + "Camera");
if (name.startsWith("&") && name.endsWith(";")) {
return Strings.browser.GetStringFromName(name.substring(1, name.length -1));
}
if (device.name.startsWith("&") && device.name.endsWith(";"))
return Strings.browser.GetStringFromName(device.name.substring(1, device.name.length -1));
if (name.trim() == "") {
defaultCount++;
return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
}
return name;
}, this);
if (device.name.trim() == "") {
defaultCount++;
return Strings.browser.formatStringFromName("getUserMedia." + aType + ".default", [defaultCount], 1);
}
return device.name
}, this);
},
_addDevicesToOptions: function(aDevices, aType, aOptions, aHost, aContext) {
if (aDevices.length == 0) {
return Promise.resolve(aOptions);
}
_addDevicesToOptions: function(aDevices, aType, aOptions, extraOptions) {
if (aDevices.length) {
let updateOptions = () => {
// Filter out empty items from the list
let list = this._getList(aDevices, aType);
if (extraOptions)
list = list.concat(extraOptions);
if (list.length > 0) {
aOptions.inputs.push({
id: aType,
@ -164,96 +152,40 @@ var WebrtcUI = {
label: Strings.browser.GetStringFromName("getUserMedia." + aType + ".prompt"),
values: list
});
}
return aOptions;
}
return this.getDefaultDevice(aType, aHost, aContext).then((defaultDevice) => {
aDevices.sort((a, b) => {
if (b.name === defaultDevice) return 1;
return 0;
});
return updateOptions();
}).catch(updateOptions);
},
// Sets the default for a aHost. If no aHost is specified, sets the browser wide default.
// Saving is async, but this doesn't wait for a result.
setDefaultDevice: function(aType, aValue, aHost, aContext) {
if (aHost) {
contentPrefs.set(aHost, "webrtc." + aType, aValue, aContext);
} else {
contentPrefs.setGlobal("webrtc." + aType, aValue, aContext);
}
},
_checkContentPref(aHost, aType, aContext) {
return new Promise((resolve, reject) => {
let result = null;
let handler = {
handleResult: (aResult) => result = aResult,
handleCompletion: function(aReason) {
if (aReason == Components.interfaces.nsIContentPrefCallback2.COMPLETE_OK &&
result instanceof Components.interfaces.nsIContentPref) {
resolve(result.value);
} else {
reject(result);
}
}
};
if (aHost) {
contentPrefs.getByDomainAndName(aHost, "webrtc." + aType, aContext, handler);
} else {
contentPrefs.getGlobal("webrtc." + aType, aContext, handler);
}
});
},
// Returns the default device for this aHost. If no aHost is specified, returns a browser wide default
getDefaultDevice: function(aType, aHost, aContext) {
return this._checkContentPref(aHost, aType, aContext).catch(() => {
// If we found nothing for the initial pref, try looking for a global one
return this._checkContentPref(null, aType, aContext);
});
},
prompt: function (aWindow, aCallID, aAudioRequested, aVideoRequested, aDevices) {
prompt: function prompt(aContentWindow, aCallID, aAudioRequested,
aVideoRequested, aDevices) {
let audioDevices = [];
let videoDevices = [];
// Split up all the available aDevices into audio and video categories
for (let device of aDevices) {
device = device.QueryInterface(Ci.nsIMediaDevice);
switch (device.type) {
case "audio":
if (aAudioRequested) {
if (aAudioRequested)
audioDevices.push(device);
}
break;
case "video":
if (aVideoRequested) {
if (aVideoRequested)
videoDevices.push(device);
}
break;
}
}
// Bsaed on the aTypes available, setup the prompt and icon text
let requestType;
if (audioDevices.length && videoDevices.length) {
if (audioDevices.length && videoDevices.length)
requestType = "CameraAndMicrophone";
} else if (audioDevices.length) {
else if (audioDevices.length)
requestType = "Microphone";
} else if (videoDevices.length) {
else if (videoDevices.length)
requestType = "Camera";
} else {
else
return;
}
let host = aWindow.document.documentURIObject.host;
// Show the app name if this is a WebRT app, otherwise show the host.
let host = aContentWindow.document.documentURIObject.host;
let requestor = BrowserApp.manifest ? "'" + BrowserApp.manifest.name + "'" : host;
let message = Strings.browser.formatStringFromName("getUserMedia.share" + requestType + ".message", [ requestor ], 1);
@ -261,20 +193,24 @@ var WebrtcUI = {
// if the users only option would be to select "No Audio" or "No Video"
// i.e. we're only showing audio or only video and there is only one device for that type
// don't bother showing a menulist to select from
if (videoDevices.length > 0 && audioDevices.length > 0) {
videoDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.videoSource.none") });
audioDevices.push({ name: Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") });
var extraItems = null;
if (videoDevices.length > 1 || audioDevices.length > 0) {
// Only show the No Video option if there are also Audio devices to choose from
if (audioDevices.length > 0)
extraItems = [ Strings.browser.GetStringFromName("getUserMedia.videoSource.none") ];
// videoSource is both the string used for l10n lookup and the object that will be returned
this._addDevicesToOptions(videoDevices, "videoSource", options, extraItems);
}
let loadContext = aWindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsILoadContext);
// videoSource is both the string used for l10n lookup and the object that will be returned
this._addDevicesToOptions(videoDevices, this.VIDEO_SOURCE, options, host, loadContext).then((aOptions) => {
return this._addDevicesToOptions(audioDevices, this.AUDIO_SOURCE, aOptions, host, loadContext);
}).catch(Cu.reportError).then((aOptions) => {
let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID, host);
NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, aOptions);
});
if (audioDevices.length > 1 || videoDevices.length > 0) {
// Only show the No Audio option if there are also Video devices to choose from
if (videoDevices.length > 0)
extraItems = [ Strings.browser.GetStringFromName("getUserMedia.audioDevice.none") ];
this._addDevicesToOptions(audioDevices, "audioDevice", options, extraItems);
}
let buttons = this.getDeviceButtons(audioDevices, videoDevices, aCallID);
NativeWindow.doorhanger.show(message, "webrtc-request", buttons, BrowserApp.selectedTab.id, options);
}
}

View File

@ -11,6 +11,10 @@ const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/Services.jsm");
const WEBRTC_PLAYER_NAME = "WebRTC Player";
const MIRROR_PORT = 8011;
const JSON_MESSAGE_TERMINATOR = "\r\n";
function log(msg) {
//Services.console.logStringMessage(msg);
}
@ -29,13 +33,14 @@ function RokuApp(service) {
#else
this.app = "Firefox Nightly";
#endif
this.appID = -1;
this.mediaAppID = -1;
this.mirrorAppID = -1;
}
RokuApp.prototype = {
status: function status(callback) {
// We have no way to know if the app is running, so just return "unknown"
// but we use this call to fetch the appID for the given app name
// but we use this call to fetch the mediaAppID for the given app name
let url = this.resourceURL + "query/apps";
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.open("GET", url, true);
@ -48,7 +53,9 @@ RokuApp.prototype = {
let apps = doc.querySelectorAll("app");
for (let app of apps) {
if (app.textContent == this.app) {
this.appID = app.id;
this.mediaAppID = app.id;
} else if (app.textContent == WEBRTC_PLAYER_NAME) {
this.mirrorAppID = app.id
}
}
}
@ -69,11 +76,11 @@ RokuApp.prototype = {
},
start: function start(callback) {
// We need to make sure we have cached the appID
if (this.appID == -1) {
// We need to make sure we have cached the mediaAppID
if (this.mediaAppID == -1) {
this.status(function() {
// If we found the appID, use it to make a new start call
if (this.appID != -1) {
// If we found the mediaAppID, use it to make a new start call
if (this.mediaAppID != -1) {
this.start(callback);
} else {
// We failed to start the app, so let the caller know
@ -85,7 +92,7 @@ RokuApp.prototype = {
// Start a given app with any extra query data. Each app uses it's own data scheme.
// NOTE: Roku will also pass "source=external-control" as a param
let url = this.resourceURL + "launch/" + this.appID + "?version=" + parseInt(PROTOCOL_VERSION);
let url = this.resourceURL + "launch/" + this.mediaAppID + "?version=" + parseInt(PROTOCOL_VERSION);
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", url, true);
xhr.overrideMimeType("text/plain");
@ -129,7 +136,7 @@ RokuApp.prototype = {
},
remoteMedia: function remoteMedia(callback, listener) {
if (this.appID != -1) {
if (this.mediaAppID != -1) {
if (callback) {
callback(new RemoteMedia(this.resourceURL, listener));
}
@ -138,6 +145,44 @@ RokuApp.prototype = {
callback();
}
}
},
mirror: function(callback, win, viewport, mirrorStartedCallback) {
if (this.mirrorAppID == -1) {
// The status function may not have been called yet if mirrorAppID is -1
this.status(this._createRemoteMirror.bind(this, callback, win, viewport, mirrorStartedCallback));
} else {
this._createRemoteMirror(callback, win, viewport, mirrorStartedCallback);
}
},
_createRemoteMirror: function(callback, win, viewport, mirrorStartedCallback) {
if (this.mirrorAppID == -1) {
// TODO: Inform user to install Roku WebRTC Player Channel.
log("RokuApp: Failed to find Mirror App ID.");
} else {
let url = this.resourceURL + "launch/" + this.mirrorAppID;
let xhr = Cc["@mozilla.org/xmlextras/xmlhttprequest;1"].createInstance(Ci.nsIXMLHttpRequest);
xhr.open("POST", url, true);
xhr.overrideMimeType("text/plain");
xhr.addEventListener("load", (function() {
// 204 seems to be returned if the channel is already running
if ((xhr.status == 200) || (xhr.status == 204)) {
this.remoteMirror = new RemoteMirror(this.resourceURL, win, viewport, mirrorStartedCallback);
}
}).bind(this), false);
xhr.addEventListener("error", function() {
log("RokuApp: XHR Failed to launch application: " + WEBRTC_PLAYER_NAME);
}, false);
xhr.send(null);
}
if (callback) {
callback();
}
}
}
@ -225,11 +270,153 @@ RemoteMedia.prototype = {
this._sendMsg({ type: "STOP" });
},
load: function load(aData) {
this._sendMsg({ type: "LOAD", title: aData.title, source: aData.source, poster: aData.poster });
load: function load(data) {
this._sendMsg({ type: "LOAD", title: data.title, source: data.source, poster: data.poster });
},
get status() {
return this._status;
}
}
function RemoteMirror(url, win, viewport, mirrorStartedCallback) {
this._serverURI = Services.io.newURI(url , null, null);
this._window = win;
this._iceCandidates = [];
this.mirrorStarted = mirrorStartedCallback;
// This code insures the generated tab mirror is not wider than 800 nor taller than 600
// Better dimensions should be chosen after the Roku Channel is working.
let windowId = win.BrowserApp.selectedBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils).outerWindowID;
let cWidth = Math.max(viewport.cssWidth, viewport.width);
let cHeight = Math.max(viewport.cssHeight, viewport.height);
const MAX_WIDTH = 800;
const MAX_HEIGHT = 600;
let tWidth = 0;
let tHeight = 0;
if ((cWidth / MAX_WIDTH) > (cHeight / MAX_HEIGHT)) {
tHeight = Math.ceil((MAX_WIDTH / cWidth) * cHeight);
tWidth = MAX_WIDTH;
} else {
tWidth = Math.ceil((MAX_HEIGHT / cHeight) * cWidth);
tHeight = MAX_HEIGHT;
}
let constraints = {
video: {
mediaSource: "browser",
browserWindow: windowId,
scrollWithPage: true,
advanced: [
{
width: { min: tWidth, max: tWidth },
height: { min: tHeight, max: tHeight }
},
{ aspectRatio: cWidth / cHeight }
]
}
};
this._window.navigator.mozGetUserMedia(constraints, this._onReceiveGUMStream.bind(this), function() {});
}
RemoteMirror.prototype = {
_sendOffer: function(offer) {
if (!this._baseSocket) {
this._baseSocket = Cc["@mozilla.org/tcp-socket;1"].createInstance(Ci.nsIDOMTCPSocket);
}
this._jsonOffer = JSON.stringify(offer);
this._socket = this._baseSocket.open(this._serverURI.host, MIRROR_PORT, { useSecureTransport: false, binaryType: "string" });
this._socket.onopen = this._onSocketOpen.bind(this);
this._socket.ondata = this._onSocketData.bind(this);
this._socket.onerror = this._onSocketError.bind(this);
},
_onReceiveGUMStream: function(stream) {
this._pc = new this._window.mozRTCPeerConnection;
this._pc.addStream(stream);
this._pc.onicecandidate = (evt => {
// Usually the last candidate is null, expected?
if (!evt.candidate) {
return;
}
let jsonCandidate = JSON.stringify(evt.candidate);
this._iceCandidates.push(jsonCandidate);
this._sendIceCandidates();
});
this._pc.createOffer(offer => {
this._pc.setLocalDescription(
new this._window.mozRTCSessionDescription(offer),
() => this._sendOffer(offer),
() => log("RemoteMirror: Failed to set local description."));
},
() => log("RemoteMirror: Failed to create offer."));
},
_stopMirror: function() {
if (this._socket) {
this._socket.close();
this._socket = null;
}
if (this._pc) {
this._pc.close();
this._pc = null;
}
this._jsonOffer = null;
this._iceCandidates = [];
},
_onSocketData: function(response) {
if (response.type == "data") {
response.data.split(JSON_MESSAGE_TERMINATOR).forEach(data => {
if (data) {
let parsedData = JSON.parse(data);
if (parsedData.type == "answer") {
this._pc.setRemoteDescription(
new this._window.mozRTCSessionDescription(parsedData),
() => this.mirrorStarted(this._stopMirror.bind(this)),
() => log("RemoteMirror: Failed to set remote description."));
} else {
this._pc.addIceCandidate(new this._window.mozRTCIceCandidate(parsedData))
}
} else {
log("RemoteMirror: data is null");
}
});
} else if (response.type == "error") {
log("RemoteMirror: Got socket error.");
this._stopMirror();
} else {
log("RemoteMirror: Got unhandled socket event: " + response.type);
}
},
_onSocketError: function(err) {
log("RemoteMirror: Error socket.onerror: " + (err.data ? err.data : "NO DATA"));
this._stopMirror();
},
_onSocketOpen: function() {
this._open = true;
if (this._jsonOffer) {
let jsonOffer = this._jsonOffer + JSON_MESSAGE_TERMINATOR;
this._socket.send(jsonOffer, jsonOffer.length);
this._jsonOffer = null;
this._sendIceCandidates();
}
},
_sendIceCandidates: function() {
if (this._socket && this._open) {
this._iceCandidates.forEach(value => {
value = value + JSON_MESSAGE_TERMINATOR;
this._socket.send(value, value.length);
});
this._iceCandidates = [];
}
}
};

View File

@ -409,6 +409,10 @@ var SimpleServiceDiscovery = {
// Only add and notify if we don't already know about this service
if (!this._services.has(service.uuid)) {
let device = this._devices.get(service.target);
if (device && device.mirror) {
service.mirror = true;
}
this._services.set(service.uuid, service);
Services.obs.notifyObservers(null, EVENT_SERVICE_FOUND, service.uuid);
}

View File

@ -0,0 +1,295 @@
/* 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/. */
"use strict";
/**
* This module provides an asynchronous API for managing bookmarks.
*
* Bookmarks are organized in a tree structure, and can be bookmarked URIs,
* folders or separators. Multiple bookmarks for the same URI are allowed.
*
* Note that if you are handling bookmarks operations in the UI, you should
* not use this API directly, but rather use PlacesTransactions.jsm, so that
* any operation is undo/redo-able.
*
* Each bookmarked item is represented by an object having the following
* properties:
*
* - guid (string)
* The globally unique identifier of the item.
* - parentGuid (string)
* The globally unique identifier of the folder containing the item.
* This will be an empty string for the Places root folder.
* - index (number)
* The 0-based position of the item in the parent folder.
* - dateAdded (number, microseconds from the epoch)
* The time at which the item was added. This is a PRTime (microseconds).
* - lastModified (number, microseconds from the epoch)
* The time at which the item was last modified. This is a PRTime (microseconds).
* - type (number)
* The item's type, either TYPE_BOOKMARK, TYPE_FOLDER or TYPE_SEPARATOR.
*
* The following properties are only valid for bookmarks or folders.
*
* - title (string)
* The item's title, if any. Empty titles and null titles are considered
* the same and the property is unset on retrieval in such a case.
*
* The following properties are only valid for bookmarks:
*
* - uri (nsIURI)
* The item's URI.
* - keyword (string)
* The associated keyword, if any.
*
* Each successful operation notifies through the nsINavBookmarksObserver
* interface. To listen to such notifications you must register using
* nsINavBookmarksService addObserver and removeObserver methods.
* Note that bookmark addition or order changes won't notify onItemMoved for
* items that have their indexes changed.
* Similarly, lastModified changes not done explicitly (like changing another
* property) won't fire an onItemChanged notification for the lastModified
* property.
* @see nsINavBookmarkObserver
*
* @note livemarks are implemented as empty folders.
* @see mozIAsyncLivemarks.idl
*/
this.EXPORTED_SYMBOLS = [ "Bookmarks" ];
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
const URI_LENGTH_MAX = 65536;
const TITLE_LENGTH_MAX = 4096;
let Bookmarks = Object.freeze({
/**
* Item's type constants.
* These should stay consistent with nsINavBookmarksService.idl
*/
TYPE_BOOKMARK: 1,
TYPE_FOLDER: 2,
TYPE_SEPARATOR: 3,
/**
* Creates or updates a bookmarked item.
*
* If the given guid is found the corresponding item is updated, otherwise,
* if no guid is provided, a bookmark is created and a new guid is assigned
* to it.
*
* In the creation case, a minimum set of properties must be provided:
* - type
* - parentGuid
* - URI, only for bookmarks
* If an index is not specified, it defaults to appending.
* It's also possible to pass a non-existent guid to force creation of an
* item with the given guid, but unless you have a very sound reason, such as
* an undo manager implementation or synchronization, you should not do that.
*
* In the update case, you should only set the properties which should be
* changed, undefined properties won't be taken into account for the update.
* Moreover, the item's type and the guid are ignored, since they are
* immutable after creation. Note that if the passed in values are not
* coherent with the known values, this rejects.
* Passing null or an empty string as keyword clears any keyword
* associated with this bookmark.
*
* Note that any known property that doesn't apply to the specific item type
* causes rejection.
*
* @param info
* object representing a bookmarked item, as defined above.
*
* @return {Promise} resolved when the update is complete.
* @resolves to the input object, updated with relevant information.
* @rejects JavaScript exception.
*
* @note title is truncated to TITLE_LENGTH_MAX and URI is rejected if
* greater than URI_LENGTH_MAX.
*/
// XXX WIP XXX Will replace functionality from these methods:
// long long insertBookmark(in long long aParentId, in nsIURI aURI, in long aIndex, in AUTF8String aTitle, [optional] in ACString aGUID);
// long long createFolder(in long long aParentFolder, in AUTF8String name, in long index, [optional] in ACString aGUID);
// void moveItem(in long long aItemId, in long long aNewParentId, in long aIndex);
// long long insertSeparator(in long long aParentId, in long aIndex, [optional] in ACString aGUID);
// void setItemTitle(in long long aItemId, in AUTF8String aTitle);
// void setItemDateAdded(in long long aItemId, in PRTime aDateAdded);
// void setItemLastModified(in long long aItemId, in PRTime aLastModified);
// void changeBookmarkURI(in long long aItemId, in nsIURI aNewURI);
// void setKeywordForBookmark(in long long aItemId, in AString aKeyword);
update: Task.async(function* (info) {
throw new Error("Not yet implemented");
}),
/**
* Removes a bookmarked item.
*
* Input can either be a guid or an object with one of the following
* properties set:
* - guid: if set, only the corresponding item is removed.
* - parentGuid: if it's set and is a folder, any children of that folder is
* removed, but not the folder itself.
* - URI: if set, any bookmark for that URI is removed.
* If multiple of these properties are set, the method rejects.
*
* Any other property is ignored, known properties may be overwritten.
*
* @param guidOrInfo
* The globally unique identifier of the item to remove, or an
* object representing it, as defined above.
*
* @return {Promise} resolved when the removal is complete.
* @resolves to the removed object or an array of them.
* @rejects JavaScript exception.
*/
// XXX WIP XXX Will replace functionality from these methods:
// removeItem(in long long aItemId);
// removeFolderChildren(in long long aItemId);
remove: Task.async(function* (guidOrInfo) {
throw new Error("Not yet implemented");
}),
/**
* Fetches information about a bookmarked item.
*
* Input can be either a guid or an object with one, and only one, of these
* filtering properties set:
* - guid
* retrieves the item with the specified guid
* - parentGuid and index
* retrieves the item by its position
* - URI
* retrieves all items having the given URI.
* - keyword
* retrieves all items having the given keyword.
*
* Any other property is ignored. Known properties may be overwritten.
*
* @param guidOrInfo
* The globally unique identifier of the item to fetch, or an
* object representing it, as defined above.
*
* @return {Promise} resolved when the fetch is complete.
* @resolves to an object representing the found item, as described above, or
* an array of such objects. if no item is found, the returned
* promise is resolved to null.
* @rejects JavaScript exception.
*/
// XXX WIP XXX Will replace functionality from these methods:
// long long getIdForItemAt(in long long aParentId, in long aIndex);
// AUTF8String getItemTitle(in long long aItemId);
// PRTime getItemDateAdded(in long long aItemId);
// PRTime getItemLastModified(in long long aItemId);
// nsIURI getBookmarkURI(in long long aItemId);
// long getItemIndex(in long long aItemId);
// unsigned short getItemType(in long long aItemId);
// boolean isBookmarked(in nsIURI aURI);
// long long getFolderIdForItem(in long long aItemId);
// void getBookmarkIdsForURI(in nsIURI aURI, [optional] out unsigned long count, [array, retval, size_is(count)] out long long bookmarks);
// AString getKeywordForURI(in nsIURI aURI);
// AString getKeywordForBookmark(in long long aItemId);
// nsIURI getURIForKeyword(in AString keyword);
fetch: Task.async(function* (guidOrInfo) {
throw new Error("Not yet implemented");
}),
/**
* Retrieves an object representation of a bookmarked item, along with all of
* its descendants, if any.
*
* Each node in the tree is an object that extends
* the item representation described above with some additional properties:
*
* - [deprecated] id (number)
* the item's id. Defined only if aOptions.includeItemIds is set.
* - annos (array)
* the item's annotations. This is not set if there are no annotations
* set for the item.
*
* The root object of the tree also has the following properties set:
* - itemsCount (number, not enumerable)
* the number of items, including the root item itself, which are
* represented in the resolved object.
*
* Bookmarked URIs may also have the following properties:
* - tags (string)
* csv string of the bookmark's tags, if any.
* - charset (string)
* the last known charset of the bookmark, if any.
* - iconuri (string)
* the bookmark's favicon URL, if any.
*
* Folders may also have the following properties:
* - children (array)
* the folder's children information, each of them having the same set of
* properties as above.
*
* @param [optional] guid
* the topmost item to be queried. If it's not passed, the Places
* root folder is queried: that is, you get a representation of the
* entire bookmarks hierarchy.
* @param [optional] options
* Options for customizing the query behavior, in the form of an
* object with any of the following properties:
* - excludeItemsCallback: a function for excluding items, along with
* their descendants. Given an item object (that has everything set
* apart its potential children data), it should return true if the
* item should be excluded. Once an item is excluded, the function
* isn't called for any of its descendants. This isn't called for
* the root item.
* WARNING: since the function may be called for each item, using
* this option can slow down the process significantly if the
* callback does anything that's not relatively trivial. It is
* highly recommended to avoid any synchronous I/O or DB queries.
* - includeItemIds: opt-in to include the deprecated id property.
* Use it if you must. It'll be removed once the switch to guids is
* complete.
*
* @return {Promise} resolved when the fetch is complete.
* @resolves to an object that represents either a single item or a
* bookmarks tree. if guid points to a non-existent item, the
* returned promise is resolved to null.
* @rejects JavaScript exception.
*/
// XXX WIP XXX: will replace functionality for these methods:
// PlacesUtils.promiseBookmarksTree()
fetchTree: Task.async(function* (guid = "", options = {}) {
throw new Error("Not yet implemented");
}),
/**
* Reorders contents of a folder based on a provided array of GUIDs.
*
* @param parentGuid
* The globally unique identifier of the folder whose contents should
* be reordered.
* @param orderedChildrenGuids
* Ordered array of the children's GUIDs. If this list contains
* non-existing entries they will be ignored. If the list is
* incomplete, missing entries will be appended.
*
* @return {Promise} resolved when reordering is complete.
* @rejects JavaScript exception.
*/
// XXX WIP XXX Will replace functionality from these methods:
// void setItemIndex(in long long aItemId, in long aNewIndex);
reorder: Task.async(function* (parentGuid, orderedChildrenGuids) {
throw new Error("Not yet implemented");
})
});

View File

@ -25,33 +25,25 @@ this.EXPORTED_SYMBOLS = [
, "PlacesUntagURITransaction"
];
const Ci = Components.interfaces;
const Cc = Components.classes;
const Cr = Components.results;
const Cu = Components.utils;
const { classes: Cc, interfaces: Ci, results: Cr, utils: Cu } = Components;
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Services",
"resource://gre/modules/Services.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
"resource://gre/modules/NetUtil.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "OS",
"resource://gre/modules/osfile.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Sqlite",
"resource://gre/modules/Sqlite.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Task",
"resource://gre/modules/Task.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Promise",
"resource://gre/modules/Promise.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Deprecated",
"resource://gre/modules/Deprecated.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "Bookmarks",
"resource://gre/modules/Bookmarks.jsm");
// The minimum amount of transactions before starting a batch. Usually we do
// do incremental updates, a batch will cause views to completely
@ -1834,9 +1826,14 @@ XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "favicons",
"@mozilla.org/browser/favicon-service;1",
"mozIAsyncFavicons");
XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "bookmarks",
"@mozilla.org/browser/nav-bookmarks-service;1",
"nsINavBookmarksService");
XPCOMUtils.defineLazyGetter(PlacesUtils, "bookmarks", () => {
let bm = Cc["@mozilla.org/browser/nav-bookmarks-service;1"]
.getService(Ci.nsINavBookmarksService);
return Object.freeze(new Proxy(bm, {
get: (target, name) => target.hasOwnProperty(name) ? target[name]
: Bookmarks[name]
}));
});
XPCOMUtils.defineLazyServiceGetter(PlacesUtils, "annotations",
"@mozilla.org/browser/annotation-service;1",

View File

@ -62,6 +62,7 @@ if CONFIG['MOZ_PLACES']:
EXTRA_JS_MODULES += [
'BookmarkHTMLUtils.jsm',
'BookmarkJSONUtils.jsm',
'Bookmarks.jsm',
'ClusterLib.js',
'ColorAnalyzer_worker.js',
'ColorConversion.js',

View File

@ -7,6 +7,9 @@ const Cu = Components.utils;
const { DebuggerServer } = Cu.import("resource://gre/modules/devtools/dbg-server.jsm");
const { DebuggerClient } = Cu.import("resource://gre/modules/devtools/dbg-client.jsm");
const {devtools} = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const {require} = devtools;
const { FileUtils } = Cu.import("resource://gre/modules/FileUtils.jsm");
const { Services } = Cu.import("resource://gre/modules/Services.jsm");
@ -114,9 +117,46 @@ addMessageListener("install", function (aMessage) {
}
});
addMessageListener("getAppActor", function (aMessage) {
let { manifestURL } = aMessage;
let request = {type: "getAppActor", manifestURL: manifestURL};
webappActorRequest(request, function (aResponse) {
sendAsyncMessage("appActor", aResponse);
});
});
let Frames = [];
addMessageListener("addFrame", function (aMessage) {
let win = Services.wm.getMostRecentWindow("navigator:browser");
let doc = win.document;
let frame = doc.createElementNS("http://www.w3.org/1999/xhtml", "iframe");
frame.setAttribute("mozbrowser", "true");
if (aMessage.mozapp) {
frame.setAttribute("mozapp", aMessage.mozapp);
}
if (aMessage.remote) {
frame.setAttribute("remote", aMessage.remote);
}
if (aMessage.src) {
frame.setAttribute("src", aMessage.src);
}
doc.documentElement.appendChild(frame);
Frames.push(frame);
sendAsyncMessage("frameAdded");
});
addMessageListener("cleanup", function () {
webappActorRequest({type: "unwatchApps"}, function () {
gClient.close();
});
});
let AppFramesMock = {
list: function () {
return Frames;
},
addObserver: function () {},
removeObserver: function () {}
};
require("devtools/server/actors/webapps").setAppFramesMock(AppFramesMock);

View File

@ -224,6 +224,21 @@ var steps = [
info("== SETUP == Disable certified app access");
SpecialPowers.popPrefEnv(next);
},
function() {
info("== TEST == Get packaged app actor");
addFrame(
{ mozapp: PACKAGED_APP_MANIFEST, remote: true },
function () {
getAppActor(PACKAGED_APP_MANIFEST, function (response) {
let tabActor = response.actor;
ok(!!tabActor, "TabActor is correctly instanciated in child.js");
ok("actor" in tabActor, "Tab actor is available in child");
ok("consoleActor" in tabActor, "Console actor is available in child");
next();
});
});
},
function() {
info("== TEST == Uninstall packaged app");
uninstall(PACKAGED_APP_MANIFEST);
@ -309,6 +324,22 @@ function uninstall(manifestURL) {
});
}
function getAppActor(manifestURL, callback) {
mm.addMessageListener("appActor", function onAppActor(aResponse) {
mm.removeMessageListener("appActor", onAppActor);
callback(aResponse);
});
mm.sendAsyncMessage("getAppActor", { manifestURL: manifestURL });
}
function addFrame(options, callback) {
mm.addMessageListener("frameAdded", function onFrameAdded() {
mm.removeMessageListener("frameAdded", onFrameAdded);
callback();
});
mm.sendAsyncMessage("addFrame", options);
}
</script>
</pre>
</body>

View File

@ -4,10 +4,7 @@
"use strict";
let Cu = Components.utils;
let Cc = Components.classes;
let Ci = Components.interfaces;
let CC = Components.Constructor;
let {Cu, Cc, Ci} = require("chrome");
Cu.import("resource://gre/modules/NetUtil.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
@ -16,7 +13,22 @@ Cu.import("resource://gre/modules/FileUtils.jsm");
let {Promise: promise} = Cu.import("resource://gre/modules/Promise.jsm", {});
let DevToolsUtils = require("devtools/toolkit/DevToolsUtils");
let { ActorPool } = require("devtools/server/actors/common");
let { DebuggerServer } = require("devtools/server/main");
let Services = require("Services");
let AppFramesMock = null;
exports.setAppFramesMock = function (mock) {
AppFramesMock = mock;
}
DevToolsUtils.defineLazyGetter(this, "AppFrames", () => {
// Offer a way for unit test to provide a mock
if (AppFramesMock) {
return AppFramesMock;
}
try {
return Cu.import("resource://gre/modules/AppFrames.jsm", {}).AppFrames;
} catch(e) {}
@ -518,11 +530,11 @@ WebappsActor.prototype = {
// frame script. That will flush the jar cache for this app and allow
// loading fresh updated resources if we reload its document.
let FlushFrameScript = function (path) {
let jar = Components.classes["@mozilla.org/file/local;1"]
.createInstance(Components.interfaces.nsILocalFile);
let jar = Cc["@mozilla.org/file/local;1"]
.createInstance(Ci.nsILocalFile);
jar.initWithPath(path);
let obs = Components.classes["@mozilla.org/observer-service;1"]
.getService(Components.interfaces.nsIObserverService);
let obs = Cc["@mozilla.org/observer-service;1"]
.getService(Ci.nsIObserverService);
obs.notifyObservers(jar, "flush-cache-entry", null);
};
for each (let frame in self._appFrames()) {
@ -1038,4 +1050,4 @@ WebappsActor.prototype.requestTypes = {
"getIconAsDataURL": WebappsActor.prototype.getIconAsDataURL
};
DebuggerServer.addGlobalActor(WebappsActor, "webappsActor");
exports.WebappsActor = WebappsActor;

View File

@ -4,6 +4,8 @@
"use strict";
try {
let chromeGlobal = this;
// Encapsulate in its own scope to allows loading this frame script
@ -64,3 +66,7 @@ let chromeGlobal = this;
});
addMessageListener("debug:disconnect", onDisconnect);
})();
} catch(e) {
dump("Exception in app child process: " + e + "\n");
}

View File

@ -442,7 +442,11 @@ var DebuggerServer = {
});
}
this.addActors("resource://gre/modules/devtools/server/actors/webapps.js");
this.registerModule("devtools/server/actors/webapps", {
prefix: "webapps",
constructor: "WebappsActor",
type: { global: true }
});
this.registerModule("devtools/server/actors/device", {
prefix: "device",
constructor: "DeviceActor",

View File

@ -37,6 +37,12 @@ XPCOMUtils.defineLazyServiceGetter(this, "FinalizationWitnessService",
// used for logging to distinguish connection instances.
let connectionCounters = new Map();
// Tracks identifiers of wrapped connections, that are Storage connections
// opened through mozStorage and then wrapped by Sqlite.jsm to use its syntactic
// sugar API. Since these connections have an unknown origin, we use this set
// to differentiate their behavior.
let wrappedConnections = new Set();
/**
* Once `true`, reject any attempt to open or close a database.
*/
@ -66,6 +72,20 @@ function logScriptError(message) {
}
}
/**
* Gets connection identifier from its database file path.
*
* @param path
* A file string path pointing to a database file.
* @return the connection identifier.
*/
function getIdentifierByPath(path) {
let basename = OS.Path.basename(path);
let number = connectionCounters.get(basename) || 0;
connectionCounters.set(basename, number + 1);
return basename + "#" + number;
}
/**
* Barriers used to ensure that Sqlite.jsm is shutdown after all
* its clients.
@ -95,17 +115,17 @@ XPCOMUtils.defineLazyGetter(this, "Barriers", () => {
* The observer is passed the connection identifier of the database
* connection that is being finalized.
*/
let finalizationObserver = function (subject, topic, connectionIdentifier) {
let connectionData = ConnectionData.byId.get(connectionIdentifier);
let finalizationObserver = function (subject, topic, identifier) {
let connectionData = ConnectionData.byId.get(identifier);
if (connectionData === undefined) {
logScriptError("Error: Attempt to finalize unknown Sqlite connection: " +
connectionIdentifier + "\n");
identifier + "\n");
return;
}
ConnectionData.byId.delete(connectionIdentifier);
logScriptError("Warning: Sqlite connection '" + connectionIdentifier +
ConnectionData.byId.delete(identifier);
logScriptError("Warning: Sqlite connection '" + identifier +
"' was not properly closed. Auto-close triggered by garbage collection.\n");
connectionData.close();
};
@ -170,13 +190,17 @@ XPCOMUtils.defineLazyGetter(this, "Barriers", () => {
* OpenedConnection needs to use the methods in this object, it will
* dispatch its method calls here.
*/
function ConnectionData(connection, basename, number, options) {
this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." + basename,
"Conn #" + number + ": ");
function ConnectionData(connection, identifier, options={}) {
this._log = Log.repository.getLoggerWithMessagePrefix("Sqlite.Connection." +
identifier + ": ");
this._log.info("Opened");
this._dbConn = connection;
this._connectionIdentifier = basename + " Conn #" + number;
// This is a unique identifier for the connection, generated through
// getIdentifierByPath. It may be used for logging or as a key in Maps.
this._identifier = identifier;
this._open = true;
this._cachedStatements = new Map();
@ -204,10 +228,10 @@ function ConnectionData(connection, basename, number, options) {
this._closeRequested = false;
Barriers.connections.client.addBlocker(
this._connectionIdentifier + ": waiting for shutdown",
this._identifier + ": waiting for shutdown",
this._deferredClose.promise,
() => ({
identifier: this._connectionIdentifier,
identifier: this._identifier,
isCloseRequested: this._closeRequested,
hasDbConn: !!this._dbConn,
hasInProgressTransaction: !!this._inProgressTransaction,
@ -224,7 +248,7 @@ function ConnectionData(connection, basename, number, options) {
* database. Used by finalization witnesses to be able to close opened
* connections on garbage collection.
*
* Key: _connectionIdentifier of ConnectionData
* Key: _identifier of ConnectionData
* Value: ConnectionData object
*/
ConnectionData.byId = new Map();
@ -304,15 +328,23 @@ ConnectionData.prototype = Object.freeze({
// function and asyncClose() finishing. See also bug 726990.
this._open = false;
this._log.debug("Calling asyncClose().");
this._dbConn.asyncClose(() => {
// We must always close the connection at the Sqlite.jsm-level, not
// necessarily at the mozStorage-level.
let markAsClosed = () => {
this._log.info("Closed");
this._dbConn = null;
// Now that the connection is closed, no need to keep
// a blocker for Barriers.connections.
Barriers.connections.client.removeBlocker(deferred.promise);
deferred.resolve();
});
}
if (wrappedConnections.has(this._identifier)) {
wrappedConnections.delete(this._identifier);
markAsClosed();
} else {
this._log.debug("Calling asyncClose().");
this._dbConn.asyncClose(markAsClosed);
}
},
executeCached: function (sql, params=null, onRow=null) {
@ -722,12 +754,7 @@ function openConnection(options) {
}
let file = FileUtils.File(path);
let basename = OS.Path.basename(path);
let number = connectionCounters.get(basename) || 0;
connectionCounters.set(basename, number + 1);
let identifier = basename + "#" + number;
let identifier = getIdentifierByPath(path);
log.info("Opening database: " + path + " (" + identifier + ")");
let deferred = Promise.defer();
@ -746,8 +773,8 @@ function openConnection(options) {
log.info("Connection opened");
try {
deferred.resolve(
new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection), basename, number,
openedOptions));
new OpenedConnection(connection.QueryInterface(Ci.mozIStorageAsyncConnection),
identifier, openedOptions));
} catch (ex) {
log.warn("Could not open database: " + CommonUtils.exceptionStr(ex));
deferred.reject(ex);
@ -759,7 +786,7 @@ function openConnection(options) {
/**
* Creates a clone of an existing and open Storage connection. The clone has
* the same underlying characteristics of the original connection and is
* returned in form of on OpenedConnection handle.
* returned in form of an OpenedConnection handle.
*
* The following parameters can control the cloned connection:
*
@ -812,10 +839,7 @@ function cloneStorageConnection(options) {
}
let path = source.databaseFile.path;
let basename = OS.Path.basename(path);
let number = connectionCounters.get(basename) || 0;
connectionCounters.set(basename, number + 1);
let identifier = basename + "#" + number;
let identifier = getIdentifierByPath(path);
log.info("Cloning database: " + path + " (" + identifier + ")");
let deferred = Promise.defer();
@ -828,8 +852,7 @@ function cloneStorageConnection(options) {
log.info("Connection cloned");
try {
let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
deferred.resolve(new OpenedConnection(conn, basename, number,
openedOptions));
deferred.resolve(new OpenedConnection(conn, identifier, openedOptions));
} catch (ex) {
log.warn("Could not clone database: " + CommonUtils.exceptionStr(ex));
deferred.reject(ex);
@ -838,6 +861,55 @@ function cloneStorageConnection(options) {
return deferred.promise;
}
/**
* Wraps an existing and open Storage connection with Sqlite.jsm API. The
* wrapped connection clone has the same underlying characteristics of the
* original connection and is returned in form of an OpenedConnection handle.
*
* Clients are responsible for closing both the Sqlite.jsm wrapper and the
* underlying mozStorage connection.
*
* The following parameters can control the wrapped connection:
*
* connection -- (mozIStorageAsyncConnection) The original Storage connection
* to wrap.
*
* @param options
* (Object) Parameters to control connection and wrap options.
*
* @return Promise<OpenedConnection>
*/
function wrapStorageConnection(options) {
let log = Log.repository.getLogger("Sqlite.ConnectionWrapper");
let connection = options && options.connection;
if (!connection || !(connection instanceof Ci.mozIStorageAsyncConnection)) {
throw new TypeError("connection not specified or invalid.");
}
if (isClosed) {
throw new Error("Sqlite.jsm has been shutdown. Cannot wrap connection to: " + connection.database.path);
}
let path = connection.databaseFile.path;
let identifier = getIdentifierByPath(path);
log.info("Wrapping database: " + path + " (" + identifier + ")");
return new Promise(resolve => {
try {
let conn = connection.QueryInterface(Ci.mozIStorageAsyncConnection);
let wrapper = new OpenedConnection(conn, identifier);
// We must not handle shutdown of a wrapped connection, since that is
// already handled by the opener.
wrappedConnections.add(identifier);
resolve(wrapper);
} catch (ex) {
log.warn("Could not wrap database: " + CommonUtils.exceptionStr(ex));
throw ex;
}
});
}
/**
* Handle on an opened SQLite database.
*
@ -877,25 +949,24 @@ function cloneStorageConnection(options) {
*
* @param connection
* (mozIStorageConnection) Underlying SQLite connection.
* @param basename
* (string) The basename of this database name. Used for logging.
* @param number
* (Number) The connection number to this database.
* @param options
* @param identifier
* (string) The unique identifier of this database. It may be used for
* logging or as a key in Maps.
* @param options [optional]
* (object) Options to control behavior of connection. See
* `openConnection`.
*/
function OpenedConnection(connection, basename, number, options) {
function OpenedConnection(connection, identifier, options={}) {
// Store all connection data in a field distinct from the
// witness. This enables us to store an additional reference to this
// field without preventing garbage collection of
// OpenedConnection. On garbage collection, we will still be able to
// close the database using this extra reference.
this._connectionData = new ConnectionData(connection, basename, number, options);
this._connectionData = new ConnectionData(connection, identifier, options);
// Store the extra reference in a map with connection identifier as
// key.
ConnectionData.byId.set(this._connectionData._connectionIdentifier,
ConnectionData.byId.set(this._connectionData._identifier,
this._connectionData);
// Make a finalization witness. If this object is garbage collected
@ -904,7 +975,7 @@ function OpenedConnection(connection, basename, number, options) {
// connection identifier string of the database.
this._witness = FinalizationWitnessService.make(
"sqlite-finalization-witness",
this._connectionData._connectionIdentifier);
this._connectionData._identifier);
}
OpenedConnection.prototype = Object.freeze({
@ -964,8 +1035,8 @@ OpenedConnection.prototype = Object.freeze({
// Unless cleanup has already been done by a previous call to
// `close`, delete the database entry from map and tell the
// finalization witness to forget.
if (ConnectionData.byId.has(this._connectionData._connectionIdentifier)) {
ConnectionData.byId.delete(this._connectionData._connectionIdentifier);
if (ConnectionData.byId.has(this._connectionData._identifier)) {
ConnectionData.byId.delete(this._connectionData._identifier);
this._witness.forget();
}
return this._connectionData.close();
@ -1175,6 +1246,7 @@ OpenedConnection.prototype = Object.freeze({
this.Sqlite = {
openConnection: openConnection,
cloneStorageConnection: cloneStorageConnection,
wrapStorageConnection: wrapStorageConnection,
/**
* Shutdown barrier client. May be used by clients to perform last-minute
* cleanup prior to the shutdown of this module.

View File

@ -845,12 +845,12 @@ add_task(function test_direct() {
add_task(function* test_cloneStorageConnection() {
let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir,
"test_cloneStorageConnection.sqlite"));
let c = yield new Promise((success, failure) => {
let c = yield new Promise((resolve, reject) => {
Services.storage.openAsyncDatabase(file, null, (status, db) => {
if (Components.isSuccessCode(status)) {
success(db.QueryInterface(Ci.mozIStorageAsyncConnection));
resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
} else {
failure(new Error(status));
reject(new Error(status));
}
});
});
@ -913,6 +913,33 @@ add_task(function* test_readOnly_clone() {
yield clone.close();
});
/**
* Test Sqlite.wrapStorageConnection.
*/
add_task(function* test_wrapStorageConnection() {
let file = new FileUtils.File(OS.Path.join(OS.Constants.Path.profileDir,
"test_wrapStorageConnection.sqlite"));
let c = yield new Promise((resolve, reject) => {
Services.storage.openAsyncDatabase(file, null, (status, db) => {
if (Components.isSuccessCode(status)) {
resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection));
} else {
reject(new Error(status));
}
});
});
let wrapper = yield Sqlite.wrapStorageConnection({ connection: c });
// Just check that it works.
yield wrapper.execute("SELECT 1");
yield wrapper.executeCached("SELECT 1");
// Closing the wrapper should just finalize statements but not close the
// database.
yield wrapper.close();
yield c.asyncClose();
});
/**
* Test finalization
*/
@ -921,7 +948,7 @@ add_task(function* test_closed_by_witness() {
let c = yield getDummyDatabase("closed_by_witness");
Services.obs.notifyObservers(null, "sqlite-finalization-witness",
c._connectionData._connectionIdentifier);
c._connectionData._identifier);
// Since we triggered finalization ourselves, tell the witness to
// forget the connection so it does not trigger a finalization again
c._witness.forget();
@ -933,7 +960,7 @@ add_task(function* test_closed_by_witness() {
add_task(function* test_warning_message_on_finalization() {
failTestsOnAutoClose(false);
let c = yield getDummyDatabase("warning_message_on_finalization");
let connectionIdentifier = c._connectionData._connectionIdentifier;
let identifier = c._connectionData._identifier;
let deferred = Promise.defer();
let listener = {
@ -941,14 +968,14 @@ add_task(function* test_warning_message_on_finalization() {
let messageText = msg.message;
// Make sure the message starts with a warning containing the
// connection identifier
if (messageText.indexOf("Warning: Sqlite connection '" + connectionIdentifier + "'") !== -1) {
if (messageText.indexOf("Warning: Sqlite connection '" + identifier + "'") !== -1) {
deferred.resolve();
}
}
};
Services.console.registerListener(listener);
Services.obs.notifyObservers(null, "sqlite-finalization-witness", connectionIdentifier);
Services.obs.notifyObservers(null, "sqlite-finalization-witness", identifier);
// Since we triggered finalization ourselves, tell the witness to
// forget the connection so it does not trigger a finalization again
c._witness.forget();

View File

@ -103,7 +103,6 @@ public:
// The following content nodes have been removed from the menu system.
// We save them here for use in command handling.
nsCOMPtr<nsIContent> mAboutItemContent;
nsCOMPtr<nsIContent> mUpdateItemContent;
nsCOMPtr<nsIContent> mPrefItemContent;
nsCOMPtr<nsIContent> mQuitItemContent;

View File

@ -37,7 +37,6 @@ BOOL gSomeMenuBarPainted = NO;
// window does not have a quit or pref item. We don't need strong refs here because
// these items are always strong ref'd by their owning menu bar (instance variable).
static nsIContent* sAboutItemContent = nullptr;
static nsIContent* sUpdateItemContent = nullptr;
static nsIContent* sPrefItemContent = nullptr;
static nsIContent* sQuitItemContent = nullptr;
@ -75,8 +74,6 @@ nsMenuBarX::~nsMenuBarX()
// hidden window, thus we need to invalidate the weak references.
if (sAboutItemContent == mAboutItemContent)
sAboutItemContent = nullptr;
if (sUpdateItemContent == mUpdateItemContent)
sUpdateItemContent = nullptr;
if (sQuitItemContent == mQuitItemContent)
sQuitItemContent = nullptr;
if (sPrefItemContent == mPrefItemContent)
@ -478,13 +475,6 @@ void nsMenuBarX::AquifyMenuBar()
if (!sAboutItemContent)
sAboutItemContent = mAboutItemContent;
// Hide the software update menu item, since it belongs in the application
// menu on Mac OS X.
HideItem(domDoc, NS_LITERAL_STRING("updateSeparator"), nullptr);
HideItem(domDoc, NS_LITERAL_STRING("checkForUpdates"), getter_AddRefs(mUpdateItemContent));
if (!sUpdateItemContent)
sUpdateItemContent = mUpdateItemContent;
// remove quit item and its separator
HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitSeparator"), nullptr);
HideItem(domDoc, NS_LITERAL_STRING("menu_FileQuitItem"), getter_AddRefs(mQuitItemContent));
@ -600,7 +590,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu)
========================
= About This App = <- aboutName
= Check for Updates... = <- checkForUpdates
========================
= Preferences... = <- menu_preferences
========================
@ -644,17 +633,6 @@ nsresult nsMenuBarX::CreateApplicationMenu(nsMenuX* inMenu)
addAboutSeparator = TRUE;
}
// Add the software update menu item
itemBeingAdded = CreateNativeAppMenuItem(inMenu, NS_LITERAL_STRING("checkForUpdates"), @selector(menuItemHit:),
eCommand_ID_Update, nsMenuBarX::sNativeEventTarget);
if (itemBeingAdded) {
[sApplicationMenu addItem:itemBeingAdded];
[itemBeingAdded release];
itemBeingAdded = nil;
addAboutSeparator = TRUE;
}
// Add separator if either the About item or software update item exists
if (addAboutSeparator)
[sApplicationMenu addItem:[NSMenuItem separatorItem]];
@ -950,12 +928,6 @@ static BOOL gMenuItemsExecuteCommands = YES;
nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
return;
}
else if (tag == eCommand_ID_Update) {
nsIContent* mostSpecificContent = sUpdateItemContent;
if (menuBar && menuBar->mUpdateItemContent)
mostSpecificContent = menuBar->mUpdateItemContent;
nsMenuUtilsX::DispatchCommandTo(mostSpecificContent);
}
else if (tag == eCommand_ID_Prefs) {
nsIContent* mostSpecificContent = sPrefItemContent;
if (menuBar && menuBar->mPrefItemContent)