mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-02-10 01:08:21 +00:00
Merge fx-team to m-c. a=merge
This commit is contained in:
commit
4f09769d88
@ -711,6 +711,7 @@
|
||||
@RESPATH@/components/nsUrlClassifierHashCompleter.js
|
||||
@RESPATH@/components/nsUrlClassifierListManager.js
|
||||
@RESPATH@/components/nsUrlClassifierLib.js
|
||||
@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
|
||||
@RESPATH@/components/url-classifier.xpt
|
||||
|
||||
; GNOME hooks
|
||||
|
@ -148,8 +148,13 @@ let wrapper = {
|
||||
|
||||
if (accountData.customizeSync) {
|
||||
Services.prefs.setBoolPref(PREF_SYNC_SHOW_CUSTOMIZATION, true);
|
||||
delete accountData.customizeSync;
|
||||
}
|
||||
delete accountData.customizeSync;
|
||||
// sessionTokenContext is erroneously sent by the content server.
|
||||
// https://github.com/mozilla/fxa-content-server/issues/2766
|
||||
// To avoid having the FxA storage manager not knowing what to do with
|
||||
// it we delete it here.
|
||||
delete accountData.sessionTokenContext;
|
||||
|
||||
// We need to confirm a relink - see shouldAllowRelink for more
|
||||
let newAccountEmail = accountData.email;
|
||||
|
@ -116,8 +116,12 @@ let TrackingProtection = {
|
||||
// Add the current host in the 'trackingprotection' consumer of
|
||||
// the permission manager using a normalized URI. This effectively
|
||||
// places this host on the tracking protection allowlist.
|
||||
Services.perms.add(normalizedUrl,
|
||||
"trackingprotection", Services.perms.ALLOW_ACTION);
|
||||
if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
|
||||
PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
|
||||
} else {
|
||||
Services.perms.add(normalizedUrl,
|
||||
"trackingprotection", Services.perms.ALLOW_ACTION);
|
||||
}
|
||||
|
||||
// Telemetry for disable protection.
|
||||
this.eventsHistogram.add(1);
|
||||
@ -133,8 +137,11 @@ let TrackingProtection = {
|
||||
"https://" + gBrowser.selectedBrowser.currentURI.hostPort,
|
||||
null, null);
|
||||
|
||||
Services.perms.remove(normalizedUrl,
|
||||
"trackingprotection");
|
||||
if (PrivateBrowsingUtils.isBrowserPrivate(gBrowser.selectedBrowser)) {
|
||||
PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
|
||||
} else {
|
||||
Services.perms.remove(normalizedUrl, "trackingprotection");
|
||||
}
|
||||
|
||||
// Telemetry for enable protection.
|
||||
this.eventsHistogram.add(2);
|
||||
|
@ -431,6 +431,10 @@ tags = trackingprotection
|
||||
support-files =
|
||||
trackingPage.html
|
||||
benignPage.html
|
||||
[browser_trackingUI_5.js]
|
||||
tags = trackingprotection
|
||||
support-files =
|
||||
trackingPage.html
|
||||
[browser_typeAheadFind.js]
|
||||
skip-if = buildapp == 'mulet'
|
||||
[browser_unknownContentType_title.js]
|
||||
|
122
browser/base/content/test/general/browser_trackingUI_5.js
Normal file
122
browser/base/content/test/general/browser_trackingUI_5.js
Normal file
@ -0,0 +1,122 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
// Test that sites added to the Tracking Protection whitelist in private
|
||||
// browsing mode don't persist once the private browsing window closes.
|
||||
|
||||
const PB_PREF = "privacy.trackingprotection.pbmode.enabled";
|
||||
const TRACKING_PAGE = "http://tracking.example.org/browser/browser/base/content/test/general/trackingPage.html";
|
||||
let TrackingProtection = null;
|
||||
let browser = null;
|
||||
let {UrlClassifierTestUtils} = Cu.import("resource://testing-common/UrlClassifierTestUtils.jsm", {});
|
||||
|
||||
registerCleanupFunction(function() {
|
||||
TrackingProtection = browser = null;
|
||||
UrlClassifierTestUtils.cleanupTestTrackers();
|
||||
});
|
||||
|
||||
function hidden(sel) {
|
||||
let win = browser.ownerGlobal;
|
||||
let el = win.document.querySelector(sel);
|
||||
let display = win.getComputedStyle(el).getPropertyValue("display", null);
|
||||
return display === "none";
|
||||
}
|
||||
|
||||
function clickButton(sel) {
|
||||
let win = browser.ownerGlobal;
|
||||
let el = win.document.querySelector(sel);
|
||||
el.doCommand();
|
||||
}
|
||||
|
||||
function testTrackingPage(window) {
|
||||
info("Tracking content must be blocked");
|
||||
ok(!TrackingProtection.container.hidden, "The container is visible");
|
||||
is(TrackingProtection.content.getAttribute("state"), "blocked-tracking-content",
|
||||
'content: state="blocked-tracking-content"');
|
||||
is(TrackingProtection.icon.getAttribute("state"), "blocked-tracking-content",
|
||||
'icon: state="blocked-tracking-content"');
|
||||
|
||||
ok(!hidden("#tracking-protection-icon"), "icon is visible");
|
||||
ok(hidden("#tracking-action-block"), "blockButton is hidden");
|
||||
|
||||
ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
|
||||
ok(!hidden("#tracking-action-unblock-private"), "unblockButtonPrivate is visible");
|
||||
|
||||
// Make sure that the blocked tracking elements message appears
|
||||
ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
|
||||
ok(hidden("#tracking-loaded"), "labelTrackingLoaded is hidden");
|
||||
ok(!hidden("#tracking-blocked"), "labelTrackingBlocked is visible");
|
||||
}
|
||||
|
||||
function testTrackingPageUnblocked() {
|
||||
info("Tracking content must be white-listed and not blocked");
|
||||
ok(!TrackingProtection.container.hidden, "The container is visible");
|
||||
is(TrackingProtection.content.getAttribute("state"), "loaded-tracking-content",
|
||||
'content: state="loaded-tracking-content"');
|
||||
is(TrackingProtection.icon.getAttribute("state"), "loaded-tracking-content",
|
||||
'icon: state="loaded-tracking-content"');
|
||||
|
||||
ok(!hidden("#tracking-protection-icon"), "icon is visible");
|
||||
ok(!hidden("#tracking-action-block"), "blockButton is visible");
|
||||
ok(hidden("#tracking-action-unblock"), "unblockButton is hidden");
|
||||
|
||||
// Make sure that the blocked tracking elements message appears
|
||||
ok(hidden("#tracking-not-detected"), "labelNoTracking is hidden");
|
||||
ok(!hidden("#tracking-loaded"), "labelTrackingLoaded is visible");
|
||||
ok(hidden("#tracking-blocked"), "labelTrackingBlocked is hidden");
|
||||
}
|
||||
|
||||
add_task(function* testExceptionAddition() {
|
||||
yield UrlClassifierTestUtils.addTestTrackers();
|
||||
let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
|
||||
browser = privateWin.gBrowser;
|
||||
let tab = browser.selectedTab = browser.addTab();
|
||||
|
||||
TrackingProtection = browser.ownerGlobal.TrackingProtection;
|
||||
yield pushPrefs([PB_PREF, true]);
|
||||
|
||||
ok(TrackingProtection.enabled, "TP is enabled after setting the pref");
|
||||
|
||||
info("Load a test page containing tracking elements");
|
||||
yield promiseTabLoadEvent(tab, TRACKING_PAGE);
|
||||
|
||||
testTrackingPage(tab.ownerDocument.defaultView);
|
||||
|
||||
info("Disable TP for the page (which reloads the page)");
|
||||
let tabReloadPromise = promiseTabLoadEvent(tab);
|
||||
clickButton("#tracking-action-unblock");
|
||||
yield tabReloadPromise;
|
||||
testTrackingPageUnblocked();
|
||||
|
||||
info("Test that the exception is remembered across tabs in the same private window");
|
||||
tab = browser.selectedTab = browser.addTab();
|
||||
|
||||
info("Load a test page containing tracking elements");
|
||||
yield promiseTabLoadEvent(tab, TRACKING_PAGE);
|
||||
testTrackingPageUnblocked();
|
||||
|
||||
yield promiseWindowClosed(privateWin);
|
||||
});
|
||||
|
||||
add_task(function* testExceptionPersistence() {
|
||||
info("Open another private browsing window");
|
||||
let privateWin = yield promiseOpenAndLoadWindow({private: true}, true);
|
||||
browser = privateWin.gBrowser;
|
||||
let tab = browser.selectedTab = browser.addTab();
|
||||
|
||||
TrackingProtection = browser.ownerGlobal.TrackingProtection;
|
||||
ok(TrackingProtection.enabled, "TP is still enabled");
|
||||
|
||||
info("Load a test page containing tracking elements");
|
||||
yield promiseTabLoadEvent(tab, TRACKING_PAGE);
|
||||
|
||||
testTrackingPage(tab.ownerDocument.defaultView);
|
||||
|
||||
info("Disable TP for the page (which reloads the page)");
|
||||
let tabReloadPromise = promiseTabLoadEvent(tab);
|
||||
clickButton("#tracking-action-unblock");
|
||||
yield tabReloadPromise;
|
||||
testTrackingPageUnblocked();
|
||||
|
||||
privateWin.close();
|
||||
});
|
@ -957,9 +957,8 @@ const CustomizableWidgets = [
|
||||
type: "custom",
|
||||
label: "loop-call-button3.label",
|
||||
tooltiptext: "loop-call-button3.tooltiptext",
|
||||
privateBrowsingTooltiptext: "loop-call-button3-pb.tooltiptext",
|
||||
defaultArea: CustomizableUI.AREA_NAVBAR,
|
||||
// Not in private browsing, see bug 1108187.
|
||||
showInPrivateBrowsing: false,
|
||||
introducedInVersion: 4,
|
||||
onBuild: function(aDocument) {
|
||||
// If we're not supposed to see the button, return zip.
|
||||
@ -967,13 +966,21 @@ const CustomizableWidgets = [
|
||||
return null;
|
||||
}
|
||||
|
||||
let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(aDocument.defaultView);
|
||||
|
||||
let node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
|
||||
node.setAttribute("id", this.id);
|
||||
node.classList.add("toolbarbutton-1");
|
||||
node.classList.add("chromeclass-toolbar-additional");
|
||||
node.classList.add("badged-button");
|
||||
node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label"));
|
||||
node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext"));
|
||||
if (isWindowPrivate)
|
||||
node.setAttribute("disabled", "true");
|
||||
let tooltiptext = isWindowPrivate ?
|
||||
CustomizableUI.getLocalizedProperty(this, "privateBrowsingTooltiptext",
|
||||
[CustomizableUI.getLocalizedProperty(this, "label")]) :
|
||||
CustomizableUI.getLocalizedProperty(this, "tooltiptext");
|
||||
node.setAttribute("tooltiptext", tooltiptext);
|
||||
node.setAttribute("removable", "true");
|
||||
node.addEventListener("command", function(event) {
|
||||
aDocument.defaultView.LoopUI.togglePanel(event);
|
||||
|
@ -119,7 +119,7 @@ function configureFxAccountIdentity() {
|
||||
let storageManager = new MockFxaStorageManager();
|
||||
// and init storage with our user.
|
||||
storageManager.initialize(user);
|
||||
return new AccountState(this, storageManager);
|
||||
return new AccountState(storageManager);
|
||||
},
|
||||
getCertificate(data, keyPair, mustBeValidUntil) {
|
||||
this.cert = {
|
||||
|
@ -570,13 +570,15 @@ loop.conversationViews = (function(mozL10n) {
|
||||
|
||||
var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
|
||||
mixins: [
|
||||
loop.store.StoreMixin("conversationStore"),
|
||||
sharedMixins.MediaSetupMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
// local
|
||||
audio: React.PropTypes.object,
|
||||
// We pass conversationStore here rather than use the mixin, to allow
|
||||
// easy configurability for the ui-showcase.
|
||||
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
// The poster URLs are for UI-showcase testing and development.
|
||||
localPosterUrl: React.PropTypes.string,
|
||||
@ -597,7 +599,17 @@ loop.conversationViews = (function(mozL10n) {
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.getStoreState();
|
||||
return this.props.conversationStore.getStoreState();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.conversationStore.on("change", function() {
|
||||
this.setState(this.props.conversationStore.getStoreState());
|
||||
}, this);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.props.conversationStore.off("change", null, this);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@ -633,6 +645,30 @@ loop.conversationViews = (function(mozL10n) {
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Should we render a visual cue to the user (e.g. a spinner) that a local
|
||||
* stream is on its way from the camera?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_isLocalLoading: function () {
|
||||
return !this.state.localSrcVideoObject && !this.props.localPosterUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Should we render a visual cue to the user (e.g. a spinner) that a remote
|
||||
* stream is on its way from the other user?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_isRemoteLoading: function() {
|
||||
return !!(!this.state.remoteSrcVideoObject &&
|
||||
!this.props.remotePosterUrl &&
|
||||
!this.state.mediaConnected);
|
||||
},
|
||||
|
||||
shouldRenderRemoteVideo: function() {
|
||||
if (this.props.mediaConnected) {
|
||||
// If remote video is not enabled, we're muted, so we'll show an avatar
|
||||
@ -646,41 +682,32 @@ loop.conversationViews = (function(mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": !this.props.video.enabled
|
||||
});
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "video-layout-wrapper"},
|
||||
React.createElement("div", {className: "conversation"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote focus-stream"},
|
||||
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
|
||||
isLoading: false,
|
||||
mediaType: "remote",
|
||||
posterUrl: this.props.remotePosterUrl,
|
||||
srcVideoObject: this.state.remoteSrcVideoObject})
|
||||
)
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses},
|
||||
React.createElement(sharedViews.MediaView, {displayAvatar: !this.props.video.enabled,
|
||||
isLoading: false,
|
||||
mediaType: "local",
|
||||
posterUrl: this.props.localPosterUrl,
|
||||
srcVideoObject: this.state.localSrcVideoObject})
|
||||
)
|
||||
),
|
||||
React.createElement(loop.shared.views.ConversationToolbar, {
|
||||
audio: this.props.audio,
|
||||
dispatcher: this.props.dispatcher,
|
||||
edit: { visible: false, enabled: false},
|
||||
hangup: this.hangup,
|
||||
publishStream: this.publishStream,
|
||||
video: this.props.video})
|
||||
)
|
||||
React.createElement("div", {className: "desktop-call-wrapper"},
|
||||
React.createElement(sharedViews.MediaLayoutView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
displayScreenShare: false,
|
||||
isLocalLoading: this._isLocalLoading(),
|
||||
isRemoteLoading: this._isRemoteLoading(),
|
||||
isScreenShareLoading: false,
|
||||
localPosterUrl: this.props.localPosterUrl,
|
||||
localSrcVideoObject: this.state.localSrcVideoObject,
|
||||
localVideoMuted: !this.props.video.enabled,
|
||||
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
|
||||
remotePosterUrl: this.props.remotePosterUrl,
|
||||
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
|
||||
renderRemoteVideo: this.shouldRenderRemoteVideo(),
|
||||
screenSharePosterUrl: null,
|
||||
screenShareVideoObject: this.state.screenShareVideoObject,
|
||||
showContextRoomName: false,
|
||||
useDesktopPaths: true}),
|
||||
React.createElement(loop.shared.views.ConversationToolbar, {
|
||||
audio: this.props.audio,
|
||||
dispatcher: this.props.dispatcher,
|
||||
edit: { visible: false, enabled: false},
|
||||
hangup: this.hangup,
|
||||
publishStream: this.publishStream,
|
||||
video: this.props.video})
|
||||
)
|
||||
);
|
||||
}
|
||||
@ -778,6 +805,7 @@ loop.conversationViews = (function(mozL10n) {
|
||||
case CALL_STATES.ONGOING: {
|
||||
return (React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: !this.state.audioMuted},
|
||||
conversationStore: this.getStore(),
|
||||
dispatcher: this.props.dispatcher,
|
||||
mediaConnected: this.state.mediaConnected,
|
||||
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
|
||||
|
@ -570,13 +570,15 @@ loop.conversationViews = (function(mozL10n) {
|
||||
|
||||
var OngoingConversationView = React.createClass({
|
||||
mixins: [
|
||||
loop.store.StoreMixin("conversationStore"),
|
||||
sharedMixins.MediaSetupMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
// local
|
||||
audio: React.PropTypes.object,
|
||||
// We pass conversationStore here rather than use the mixin, to allow
|
||||
// easy configurability for the ui-showcase.
|
||||
conversationStore: React.PropTypes.instanceOf(loop.store.ConversationStore).isRequired,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
// The poster URLs are for UI-showcase testing and development.
|
||||
localPosterUrl: React.PropTypes.string,
|
||||
@ -597,7 +599,17 @@ loop.conversationViews = (function(mozL10n) {
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return this.getStoreState();
|
||||
return this.props.conversationStore.getStoreState();
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.conversationStore.on("change", function() {
|
||||
this.setState(this.props.conversationStore.getStoreState());
|
||||
}, this);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.props.conversationStore.off("change", null, this);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
@ -633,6 +645,30 @@ loop.conversationViews = (function(mozL10n) {
|
||||
}));
|
||||
},
|
||||
|
||||
/**
|
||||
* Should we render a visual cue to the user (e.g. a spinner) that a local
|
||||
* stream is on its way from the camera?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_isLocalLoading: function () {
|
||||
return !this.state.localSrcVideoObject && !this.props.localPosterUrl;
|
||||
},
|
||||
|
||||
/**
|
||||
* Should we render a visual cue to the user (e.g. a spinner) that a remote
|
||||
* stream is on its way from the other user?
|
||||
*
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_isRemoteLoading: function() {
|
||||
return !!(!this.state.remoteSrcVideoObject &&
|
||||
!this.props.remotePosterUrl &&
|
||||
!this.state.mediaConnected);
|
||||
},
|
||||
|
||||
shouldRenderRemoteVideo: function() {
|
||||
if (this.props.mediaConnected) {
|
||||
// If remote video is not enabled, we're muted, so we'll show an avatar
|
||||
@ -646,41 +682,32 @@ loop.conversationViews = (function(mozL10n) {
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var localStreamClasses = React.addons.classSet({
|
||||
local: true,
|
||||
"local-stream": true,
|
||||
"local-stream-audio": !this.props.video.enabled
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="video-layout-wrapper">
|
||||
<div className="conversation">
|
||||
<div className="media nested">
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote focus-stream">
|
||||
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
|
||||
isLoading={false}
|
||||
mediaType="remote"
|
||||
posterUrl={this.props.remotePosterUrl}
|
||||
srcVideoObject={this.state.remoteSrcVideoObject} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={localStreamClasses}>
|
||||
<sharedViews.MediaView displayAvatar={!this.props.video.enabled}
|
||||
isLoading={false}
|
||||
mediaType="local"
|
||||
posterUrl={this.props.localPosterUrl}
|
||||
srcVideoObject={this.state.localSrcVideoObject} />
|
||||
</div>
|
||||
</div>
|
||||
<loop.shared.views.ConversationToolbar
|
||||
audio={this.props.audio}
|
||||
dispatcher={this.props.dispatcher}
|
||||
edit={{ visible: false, enabled: false }}
|
||||
hangup={this.hangup}
|
||||
publishStream={this.publishStream}
|
||||
video={this.props.video} />
|
||||
</div>
|
||||
<div className="desktop-call-wrapper">
|
||||
<sharedViews.MediaLayoutView
|
||||
dispatcher={this.props.dispatcher}
|
||||
displayScreenShare={false}
|
||||
isLocalLoading={this._isLocalLoading()}
|
||||
isRemoteLoading={this._isRemoteLoading()}
|
||||
isScreenShareLoading={false}
|
||||
localPosterUrl={this.props.localPosterUrl}
|
||||
localSrcVideoObject={this.state.localSrcVideoObject}
|
||||
localVideoMuted={!this.props.video.enabled}
|
||||
matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
|
||||
remotePosterUrl={this.props.remotePosterUrl}
|
||||
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
|
||||
renderRemoteVideo={this.shouldRenderRemoteVideo()}
|
||||
screenSharePosterUrl={null}
|
||||
screenShareVideoObject={this.state.screenShareVideoObject}
|
||||
showContextRoomName={false}
|
||||
useDesktopPaths={true} />
|
||||
<loop.shared.views.ConversationToolbar
|
||||
audio={this.props.audio}
|
||||
dispatcher={this.props.dispatcher}
|
||||
edit={{ visible: false, enabled: false }}
|
||||
hangup={this.hangup}
|
||||
publishStream={this.publishStream}
|
||||
video={this.props.video} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@ -778,6 +805,7 @@ loop.conversationViews = (function(mozL10n) {
|
||||
case CALL_STATES.ONGOING: {
|
||||
return (<OngoingConversationView
|
||||
audio={{enabled: !this.state.audioMuted}}
|
||||
conversationStore={this.getStore()}
|
||||
dispatcher={this.props.dispatcher}
|
||||
mediaConnected={this.state.mediaConnected}
|
||||
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
|
||||
|
@ -665,7 +665,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_shouldRenderLocalLoading: function () {
|
||||
_isLocalLoading: function () {
|
||||
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
|
||||
!this.state.localSrcVideoObject;
|
||||
},
|
||||
@ -677,7 +677,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_shouldRenderRemoteLoading: function() {
|
||||
_isRemoteLoading: function() {
|
||||
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
|
||||
!this.state.remoteSrcVideoObject &&
|
||||
!this.state.mediaConnected);
|
||||
@ -741,63 +741,54 @@ loop.roomViews = (function(mozL10n) {
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
|
||||
return (
|
||||
React.createElement("div", {className: "room-conversation-wrapper"},
|
||||
React.createElement("div", {className: "video-layout-wrapper"},
|
||||
React.createElement("div", {className: "conversation room-conversation"},
|
||||
React.createElement("div", {className: "media nested"},
|
||||
React.createElement(DesktopRoomInvitationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
error: this.state.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
onAddContextClick: this.handleAddContextClick,
|
||||
onEditContextClose: this.handleEditContextClose,
|
||||
roomData: roomData,
|
||||
savingContext: this.state.savingContext,
|
||||
show: shouldRenderInvitationOverlay,
|
||||
showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView,
|
||||
socialShareProviders: this.state.socialShareProviders}),
|
||||
React.createElement("div", {className: "video_wrapper remote_wrapper"},
|
||||
React.createElement("div", {className: "video_inner remote focus-stream"},
|
||||
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
|
||||
isLoading: this._shouldRenderRemoteLoading(),
|
||||
mediaType: "remote",
|
||||
posterUrl: this.props.remotePosterUrl,
|
||||
srcVideoObject: this.state.remoteSrcVideoObject})
|
||||
)
|
||||
),
|
||||
React.createElement("div", {className: localStreamClasses},
|
||||
React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted,
|
||||
isLoading: this._shouldRenderLocalLoading(),
|
||||
mediaType: "local",
|
||||
posterUrl: this.props.localPosterUrl,
|
||||
srcVideoObject: this.state.localSrcVideoObject})
|
||||
),
|
||||
React.createElement(DesktopRoomEditContextView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
error: this.state.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
onClose: this.handleEditContextClose,
|
||||
roomData: roomData,
|
||||
savingContext: this.state.savingContext,
|
||||
show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
|
||||
),
|
||||
React.createElement(sharedViews.ConversationToolbar, {
|
||||
audio: {enabled: !this.state.audioMuted, visible: true},
|
||||
dispatcher: this.props.dispatcher,
|
||||
edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext},
|
||||
hangup: this.leaveRoom,
|
||||
onEditClick: this.handleEditContextClick,
|
||||
publishStream: this.publishStream,
|
||||
screenShare: screenShareData,
|
||||
video: {enabled: !this.state.videoMuted, visible: true}})
|
||||
)
|
||||
),
|
||||
React.createElement(sharedViews.chat.TextChatView, {
|
||||
React.createElement("div", {className: "room-conversation-wrapper desktop-room-wrapper"},
|
||||
React.createElement(sharedViews.MediaLayoutView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
showRoomName: false,
|
||||
useDesktopPaths: true})
|
||||
displayScreenShare: false,
|
||||
isLocalLoading: this._isLocalLoading(),
|
||||
isRemoteLoading: this._isRemoteLoading(),
|
||||
isScreenShareLoading: false,
|
||||
localPosterUrl: this.props.localPosterUrl,
|
||||
localSrcVideoObject: this.state.localSrcVideoObject,
|
||||
localVideoMuted: this.state.videoMuted,
|
||||
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
|
||||
remotePosterUrl: this.props.remotePosterUrl,
|
||||
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
|
||||
renderRemoteVideo: this.shouldRenderRemoteVideo(),
|
||||
screenSharePosterUrl: null,
|
||||
screenShareVideoObject: this.state.screenShareVideoObject,
|
||||
showContextRoomName: false,
|
||||
useDesktopPaths: true},
|
||||
React.createElement(DesktopRoomInvitationView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
error: this.state.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
onAddContextClick: this.handleAddContextClick,
|
||||
onEditContextClose: this.handleEditContextClose,
|
||||
roomData: roomData,
|
||||
savingContext: this.state.savingContext,
|
||||
show: shouldRenderInvitationOverlay,
|
||||
showEditContext: shouldRenderInvitationOverlay && shouldRenderEditContextView,
|
||||
socialShareProviders: this.state.socialShareProviders}),
|
||||
React.createElement(DesktopRoomEditContextView, {
|
||||
dispatcher: this.props.dispatcher,
|
||||
error: this.state.error,
|
||||
mozLoop: this.props.mozLoop,
|
||||
onClose: this.handleEditContextClose,
|
||||
roomData: roomData,
|
||||
savingContext: this.state.savingContext,
|
||||
show: !shouldRenderInvitationOverlay && shouldRenderEditContextView})
|
||||
),
|
||||
React.createElement(sharedViews.ConversationToolbar, {
|
||||
audio: {enabled: !this.state.audioMuted, visible: true},
|
||||
dispatcher: this.props.dispatcher,
|
||||
edit: { visible: this.state.contextEnabled, enabled: !this.state.showEditContext},
|
||||
hangup: this.leaveRoom,
|
||||
onEditClick: this.handleEditContextClick,
|
||||
publishStream: this.publishStream,
|
||||
screenShare: screenShareData,
|
||||
video: {enabled: !this.state.videoMuted, visible: true}})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -665,7 +665,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_shouldRenderLocalLoading: function () {
|
||||
_isLocalLoading: function () {
|
||||
return this.state.roomState === ROOM_STATES.MEDIA_WAIT &&
|
||||
!this.state.localSrcVideoObject;
|
||||
},
|
||||
@ -677,7 +677,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
* @returns {boolean}
|
||||
* @private
|
||||
*/
|
||||
_shouldRenderRemoteLoading: function() {
|
||||
_isRemoteLoading: function() {
|
||||
return !!(this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS &&
|
||||
!this.state.remoteSrcVideoObject &&
|
||||
!this.state.mediaConnected);
|
||||
@ -741,63 +741,54 @@ loop.roomViews = (function(mozL10n) {
|
||||
return null;
|
||||
}
|
||||
default: {
|
||||
|
||||
return (
|
||||
<div className="room-conversation-wrapper">
|
||||
<div className="video-layout-wrapper">
|
||||
<div className="conversation room-conversation">
|
||||
<div className="media nested">
|
||||
<DesktopRoomInvitationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
error={this.state.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
onAddContextClick={this.handleAddContextClick}
|
||||
onEditContextClose={this.handleEditContextClose}
|
||||
roomData={roomData}
|
||||
savingContext={this.state.savingContext}
|
||||
show={shouldRenderInvitationOverlay}
|
||||
showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView}
|
||||
socialShareProviders={this.state.socialShareProviders} />
|
||||
<div className="video_wrapper remote_wrapper">
|
||||
<div className="video_inner remote focus-stream">
|
||||
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
|
||||
isLoading={this._shouldRenderRemoteLoading()}
|
||||
mediaType="remote"
|
||||
posterUrl={this.props.remotePosterUrl}
|
||||
srcVideoObject={this.state.remoteSrcVideoObject} />
|
||||
</div>
|
||||
</div>
|
||||
<div className={localStreamClasses}>
|
||||
<sharedViews.MediaView displayAvatar={this.state.videoMuted}
|
||||
isLoading={this._shouldRenderLocalLoading()}
|
||||
mediaType="local"
|
||||
posterUrl={this.props.localPosterUrl}
|
||||
srcVideoObject={this.state.localSrcVideoObject} />
|
||||
</div>
|
||||
<DesktopRoomEditContextView
|
||||
dispatcher={this.props.dispatcher}
|
||||
error={this.state.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
onClose={this.handleEditContextClose}
|
||||
roomData={roomData}
|
||||
savingContext={this.state.savingContext}
|
||||
show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} />
|
||||
</div>
|
||||
<sharedViews.ConversationToolbar
|
||||
audio={{enabled: !this.state.audioMuted, visible: true}}
|
||||
dispatcher={this.props.dispatcher}
|
||||
edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
|
||||
hangup={this.leaveRoom}
|
||||
onEditClick={this.handleEditContextClick}
|
||||
publishStream={this.publishStream}
|
||||
screenShare={screenShareData}
|
||||
video={{enabled: !this.state.videoMuted, visible: true}} />
|
||||
</div>
|
||||
</div>
|
||||
<sharedViews.chat.TextChatView
|
||||
<div className="room-conversation-wrapper desktop-room-wrapper">
|
||||
<sharedViews.MediaLayoutView
|
||||
dispatcher={this.props.dispatcher}
|
||||
showRoomName={false}
|
||||
useDesktopPaths={true} />
|
||||
displayScreenShare={false}
|
||||
isLocalLoading={this._isLocalLoading()}
|
||||
isRemoteLoading={this._isRemoteLoading()}
|
||||
isScreenShareLoading={false}
|
||||
localPosterUrl={this.props.localPosterUrl}
|
||||
localSrcVideoObject={this.state.localSrcVideoObject}
|
||||
localVideoMuted={this.state.videoMuted}
|
||||
matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
|
||||
remotePosterUrl={this.props.remotePosterUrl}
|
||||
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
|
||||
renderRemoteVideo={this.shouldRenderRemoteVideo()}
|
||||
screenSharePosterUrl={null}
|
||||
screenShareVideoObject={this.state.screenShareVideoObject}
|
||||
showContextRoomName={false}
|
||||
useDesktopPaths={true}>
|
||||
<DesktopRoomInvitationView
|
||||
dispatcher={this.props.dispatcher}
|
||||
error={this.state.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
onAddContextClick={this.handleAddContextClick}
|
||||
onEditContextClose={this.handleEditContextClose}
|
||||
roomData={roomData}
|
||||
savingContext={this.state.savingContext}
|
||||
show={shouldRenderInvitationOverlay}
|
||||
showEditContext={shouldRenderInvitationOverlay && shouldRenderEditContextView}
|
||||
socialShareProviders={this.state.socialShareProviders} />
|
||||
<DesktopRoomEditContextView
|
||||
dispatcher={this.props.dispatcher}
|
||||
error={this.state.error}
|
||||
mozLoop={this.props.mozLoop}
|
||||
onClose={this.handleEditContextClose}
|
||||
roomData={roomData}
|
||||
savingContext={this.state.savingContext}
|
||||
show={!shouldRenderInvitationOverlay && shouldRenderEditContextView} />
|
||||
</sharedViews.MediaLayoutView>
|
||||
<sharedViews.ConversationToolbar
|
||||
audio={{enabled: !this.state.audioMuted, visible: true}}
|
||||
dispatcher={this.props.dispatcher}
|
||||
edit={{ visible: this.state.contextEnabled, enabled: !this.state.showEditContext }}
|
||||
hangup={this.leaveRoom}
|
||||
onEditClick={this.handleEditContextClick}
|
||||
publishStream={this.publishStream}
|
||||
screenShare={screenShareData}
|
||||
video={{enabled: !this.state.videoMuted, visible: true}} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
@ -504,28 +504,6 @@
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.fx-embedded .local-stream {
|
||||
position: absolute;
|
||||
right: 3px;
|
||||
bottom: 5px;
|
||||
/* next two lines are workaround for lack of object-fit; see bug 1020445 */
|
||||
max-width: 140px;
|
||||
width: 30%;
|
||||
height: 28%;
|
||||
max-height: 105px;
|
||||
}
|
||||
|
||||
.fx-embedded .local-stream.room-preview {
|
||||
top: 0px;
|
||||
left: 0px;
|
||||
right: 0px;
|
||||
bottom: 0px;
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
max-width: none;
|
||||
max-height: none;
|
||||
}
|
||||
|
||||
.conversation .media.nested .focus-stream {
|
||||
display: inline-block;
|
||||
position: absolute; /* workaround for lack of object-fit; see bug 1020445 */
|
||||
@ -592,15 +570,11 @@
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.remote .avatar {
|
||||
.remote > .avatar {
|
||||
/* make visually distinct from local avatar */
|
||||
opacity: 0.25;
|
||||
}
|
||||
|
||||
.fx-embedded .media.nested {
|
||||
min-height: 200px;
|
||||
}
|
||||
|
||||
.fx-embedded-call-identifier {
|
||||
display: inline;
|
||||
width: 100%;
|
||||
@ -675,7 +649,9 @@
|
||||
* */
|
||||
html, .fx-embedded, #main,
|
||||
.video-layout-wrapper,
|
||||
.conversation {
|
||||
.conversation,
|
||||
.desktop-call-wrapper,
|
||||
.desktop-room-wrapper {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -935,7 +911,6 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
|
||||
border-top: 2px solid #444;
|
||||
border-bottom: 2px solid #444;
|
||||
padding: .5rem;
|
||||
max-height: 400px;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
bottom: 0;
|
||||
@ -951,7 +926,7 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
|
||||
overflow-x: hidden;
|
||||
overflow-y: auto;
|
||||
/* Make the context view float atop the video elements. */
|
||||
z-index: 2;
|
||||
z-index: 3;
|
||||
}
|
||||
|
||||
.room-invitation-overlay .room-context {
|
||||
@ -1087,12 +1062,12 @@ html[dir="rtl"] .room-context-btn-close {
|
||||
.standalone-room-wrapper > .media-layout {
|
||||
/* 50px is the header, 64px for toolbar, 3em is the footer. */
|
||||
height: calc(100% - 50px - 64px - 3em);
|
||||
margin: 0 10px;
|
||||
}
|
||||
|
||||
.media-layout > .media-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column wrap;
|
||||
margin: 0 10px;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
@ -1139,6 +1114,14 @@ html[dir="rtl"] .room-context-btn-close {
|
||||
height: calc(100% - 300px);
|
||||
}
|
||||
|
||||
.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
|
||||
.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
|
||||
/* Account for height of .conversation-toolbar on desktop */
|
||||
/* When we change the toolbar in bug 1184559 we can remove this. */
|
||||
margin-top: 26px;
|
||||
height: calc(100% - 150px - 26px);
|
||||
}
|
||||
|
||||
/* Temporarily slaved from .media-wrapper until we use it in more places
|
||||
to avoid affecting the conversation window on desktop. */
|
||||
.media-wrapper > .text-chat-view > .text-chat-entries {
|
||||
@ -1204,7 +1187,7 @@ html[dir="rtl"] .room-context-btn-close {
|
||||
|
||||
/* Temporarily slaved from .media-wrapper until we use it in more places
|
||||
to avoid affecting the conversation window on desktop. */
|
||||
.media-wrapper > .text-chat-view > .text-chat-entries {
|
||||
.text-chat-view > .text-chat-entries {
|
||||
/* 40px is the height of .text-chat-box. */
|
||||
height: calc(100% - 40px);
|
||||
width: 100%;
|
||||
@ -1215,20 +1198,26 @@ html[dir="rtl"] .room-context-btn-close {
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.media-wrapper > .local {
|
||||
.media-wrapper > .focus-stream > .local {
|
||||
/* Position over the remote video */
|
||||
position: absolute;
|
||||
/* Make sure its on top */
|
||||
z-index: 1001;
|
||||
z-index: 2;
|
||||
margin: 3px;
|
||||
right: 0;
|
||||
/* 29px is (30% of 50px high header) + (height toolbar (38px) +
|
||||
height footer (25px) - height header (50px)) */
|
||||
bottom: calc(30% + 29px);
|
||||
bottom: 0;
|
||||
width: 120px;
|
||||
height: 120px;
|
||||
}
|
||||
|
||||
.standalone-room-wrapper > .media-layout > .media-wrapper > .local {
|
||||
/* Add 10px for the margin on standalone */
|
||||
right: 10px;
|
||||
}
|
||||
|
||||
|
||||
html[dir="rtl"] .media-wrapper > .local {
|
||||
right: auto;
|
||||
left: 0;
|
||||
@ -1247,6 +1236,15 @@ html[dir="rtl"] .room-context-btn-close {
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.desktop-call-wrapper > .media-layout > .media-wrapper > .text-chat-view,
|
||||
.desktop-room-wrapper > .media-layout > .media-wrapper > .text-chat-view {
|
||||
/* When we change the toolbar in bug 1184559 we can remove this. */
|
||||
/* Reset back to 0 for .conversation-toolbar override on desktop */
|
||||
margin-top: 0;
|
||||
/* This is temp, to echo the .media-wrapper > .text-chat-view above */
|
||||
height: 30%;
|
||||
}
|
||||
|
||||
.media-wrapper.receiving-screen-share > .screen {
|
||||
order: 1;
|
||||
}
|
||||
@ -1288,6 +1286,47 @@ html[dir="rtl"] .room-context-btn-close {
|
||||
}
|
||||
}
|
||||
|
||||
/* e.g. very narrow widths similar to conversation window */
|
||||
@media screen and (max-width:300px) {
|
||||
.media-layout > .media-wrapper {
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.media-wrapper > .focus-stream > .local {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
/* 30% is the height of the text chat. As we have a margin,
|
||||
we don't need to worry about any offset for a border */
|
||||
bottom: 0;
|
||||
margin: 3px;
|
||||
object-fit: contain;
|
||||
/* These make the avatar look reasonable and the local
|
||||
video not too big */
|
||||
width: 25%;
|
||||
height: 25%;
|
||||
}
|
||||
|
||||
.media-wrapper:not(.showing-remote-streams) > .focus-stream > .no-video {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.media-wrapper:not(.showing-remote-streams) > .focus-stream > .local {
|
||||
position: relative;
|
||||
margin: 0;
|
||||
right: auto;
|
||||
left: auto;
|
||||
bottom: auto;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.media-wrapper > .focus-stream {
|
||||
flex: 1 1 auto;
|
||||
height: auto;
|
||||
}
|
||||
}
|
||||
|
||||
.standalone > #main > .room-conversation-wrapper > .media-layout > .conversation-toolbar {
|
||||
border: none;
|
||||
}
|
||||
@ -1415,37 +1454,12 @@ html[dir="rtl"] .standalone .room-conversation-wrapper .room-inner-info-area {
|
||||
height: auto;
|
||||
}
|
||||
|
||||
/* Text chat in rooms styles */
|
||||
|
||||
.fx-embedded .room-conversation-wrapper {
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.fx-embedded .video-layout-wrapper {
|
||||
flex: 1 1 auto;
|
||||
}
|
||||
/* Text chat in styles */
|
||||
|
||||
.text-chat-view {
|
||||
background: white;
|
||||
}
|
||||
|
||||
.fx-embedded .text-chat-view {
|
||||
flex: 1 0 auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
}
|
||||
|
||||
.fx-embedded .text-chat-entries {
|
||||
flex: 1 1 auto;
|
||||
max-height: 120px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.fx-embedded .text-chat-view > .text-chat-entries-empty {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.text-chat-box {
|
||||
flex: 0 0 auto;
|
||||
max-height: 40px;
|
||||
@ -1740,6 +1754,47 @@ html[dir="rtl"] .text-chat-entry.received .text-chat-arrow {
|
||||
}
|
||||
}
|
||||
|
||||
/* e.g. very narrow widths similar to conversation window */
|
||||
@media screen and (max-width:300px) {
|
||||
.text-chat-view {
|
||||
flex: 0 0 auto;
|
||||
display: flex;
|
||||
flex-flow: column nowrap;
|
||||
/* 120px max-height of .text-chat-entries plus 40px of .text-chat-box */
|
||||
max-height: 160px;
|
||||
/* 60px min-height of .text-chat-entries plus 40px of .text-chat-box */
|
||||
min-height: 100px;
|
||||
/* The !important is to override the values defined above which have more
|
||||
specificity when we fix bug 1184559, we should be able to remove it,
|
||||
but this should be tests first. */
|
||||
height: auto !important;
|
||||
}
|
||||
|
||||
.text-chat-entries {
|
||||
/* The !important is to override the values defined above which have more
|
||||
specificity when we fix bug 1184559, we should be able to remove it,
|
||||
but this should be tests first. */
|
||||
flex: 1 1 auto !important;
|
||||
max-height: 120px;
|
||||
min-height: 60px;
|
||||
}
|
||||
|
||||
.text-chat-entries-empty.text-chat-disabled {
|
||||
display: none;
|
||||
}
|
||||
|
||||
/* When the text chat entries are not present, then hide the entries view
|
||||
and just show the chat box. */
|
||||
.text-chat-entries-empty {
|
||||
max-height: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
|
||||
.text-chat-entries-empty > .text-chat-entries {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
|
||||
.self-view-hidden-message {
|
||||
/* Not displayed by default; display is turned on elsewhere when the
|
||||
* self-view is actually hidden.
|
||||
|
@ -580,21 +580,6 @@ loop.store.ActiveRoomStore = (function() {
|
||||
* @param {sharedActions.ConnectionFailure} actionData
|
||||
*/
|
||||
connectionFailure: function(actionData) {
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
*/
|
||||
if (this._isDesktop &&
|
||||
actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA &&
|
||||
this.getStoreState().videoMuted === false) {
|
||||
// We failed to publish with media, so due to the bug, we try again without
|
||||
// video.
|
||||
this.setStoreState({videoMuted: true});
|
||||
this._sdkDriver.retryPublishWithoutVideo();
|
||||
return;
|
||||
}
|
||||
|
||||
var exitState = this._storeState.roomState === ROOM_STATES.FAILED ?
|
||||
this._storeState.failureExitState : this._storeState.roomState;
|
||||
|
||||
|
@ -146,21 +146,6 @@ loop.store = loop.store || {};
|
||||
* @param {sharedActions.ConnectionFailure} actionData The action data.
|
||||
*/
|
||||
connectionFailure: function(actionData) {
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
*/
|
||||
if (this._isDesktop &&
|
||||
actionData.reason === FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA &&
|
||||
this.getStoreState().videoMuted === false) {
|
||||
// We failed to publish with media, so due to the bug, we try again without
|
||||
// video.
|
||||
this.setStoreState({videoMuted: true});
|
||||
this.sdkDriver.retryPublishWithoutVideo();
|
||||
return;
|
||||
}
|
||||
|
||||
this._endSession();
|
||||
this.setStoreState({
|
||||
callState: CALL_STATES.TERMINATED,
|
||||
|
@ -61,15 +61,26 @@ loop.OTSdkDriver = (function() {
|
||||
|
||||
/**
|
||||
* XXX This is a workaround for desktop machines that do not have a
|
||||
* camera installed. As we don't yet have device enumeration, when
|
||||
* we do, this can be removed (bug 1138851), and the sdk should handle it.
|
||||
* camera installed. The SDK doesn't currently do use the new device
|
||||
* enumeration apis, when it does (bug 1138851), we can drop this part.
|
||||
*/
|
||||
if (this._isDesktop && !window.MediaStreamTrack.getSources) {
|
||||
if (this._isDesktop) {
|
||||
// If there's no getSources function, the sdk defines its own and caches
|
||||
// the result. So here we define the "normal" one which doesn't get cached, so
|
||||
// we can change it later.
|
||||
// the result. So here we define our own one which wraps around the
|
||||
// real device enumeration api.
|
||||
window.MediaStreamTrack.getSources = function(callback) {
|
||||
callback([{kind: "audio"}, {kind: "video"}]);
|
||||
navigator.mediaDevices.enumerateDevices().then(function(devices) {
|
||||
var result = [];
|
||||
devices.forEach(function(device) {
|
||||
if (device.kind === "audioinput") {
|
||||
result.push({kind: "audio"});
|
||||
}
|
||||
if (device.kind === "videoinput") {
|
||||
result.push({kind: "video"});
|
||||
}
|
||||
});
|
||||
callback(result);
|
||||
});
|
||||
};
|
||||
}
|
||||
};
|
||||
@ -109,21 +120,13 @@ loop.OTSdkDriver = (function() {
|
||||
|
||||
this.sdk.on("exception", this._onOTException.bind(this));
|
||||
|
||||
// At this state we init the publisher, even though we might be waiting for
|
||||
// the initial connect of the session. This saves time when setting up
|
||||
// the media.
|
||||
this._publishLocalStreams();
|
||||
},
|
||||
|
||||
/**
|
||||
* Internal function to publish a local stream.
|
||||
* XXX This can be simplified when bug 1138851 is actioned.
|
||||
*/
|
||||
_publishLocalStreams: function() {
|
||||
// We expect the local video to be muted automatically by the SDK. Hence
|
||||
// we don't mute it manually here.
|
||||
this._mockPublisherEl = document.createElement("div");
|
||||
|
||||
// At this state we init the publisher, even though we might be waiting for
|
||||
// the initial connect of the session. This saves time when setting up
|
||||
// the media.
|
||||
this.publisher = this.sdk.initPublisher(this._mockPublisherEl,
|
||||
_.extend(this._getDataChannelSettings, this._getCopyPublisherConfig));
|
||||
|
||||
@ -135,17 +138,6 @@ loop.OTSdkDriver = (function() {
|
||||
this._onAccessDialogOpened.bind(this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Forces the sdk into not using video, and starts publishing again.
|
||||
* XXX This is part of the work around that will be removed by bug 1138851.
|
||||
*/
|
||||
retryPublishWithoutVideo: function() {
|
||||
window.MediaStreamTrack.getSources = function(callback) {
|
||||
callback([{kind: "audio"}]);
|
||||
};
|
||||
this._publishLocalStreams();
|
||||
},
|
||||
|
||||
/**
|
||||
* Handles the setMute action. Informs the published stream to mute
|
||||
* or unmute audio as appropriate.
|
||||
|
@ -150,8 +150,7 @@ loop.shared.views.chat = (function(mozL10n) {
|
||||
var lastTimestamp = 0;
|
||||
|
||||
var entriesClasses = React.addons.classSet({
|
||||
"text-chat-entries": true,
|
||||
"text-chat-entries-empty": !this.props.messageList.length
|
||||
"text-chat-entries": true
|
||||
});
|
||||
|
||||
return (
|
||||
@ -382,7 +381,8 @@ loop.shared.views.chat = (function(mozL10n) {
|
||||
|
||||
var textChatViewClasses = React.addons.classSet({
|
||||
"text-chat-view": true,
|
||||
"text-chat-disabled": !this.state.textChatEnabled
|
||||
"text-chat-disabled": !this.state.textChatEnabled,
|
||||
"text-chat-entries-empty": !messageList.length
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -150,8 +150,7 @@ loop.shared.views.chat = (function(mozL10n) {
|
||||
var lastTimestamp = 0;
|
||||
|
||||
var entriesClasses = React.addons.classSet({
|
||||
"text-chat-entries": true,
|
||||
"text-chat-entries-empty": !this.props.messageList.length
|
||||
"text-chat-entries": true
|
||||
});
|
||||
|
||||
return (
|
||||
@ -382,7 +381,8 @@ loop.shared.views.chat = (function(mozL10n) {
|
||||
|
||||
var textChatViewClasses = React.addons.classSet({
|
||||
"text-chat-view": true,
|
||||
"text-chat-disabled": !this.state.textChatEnabled
|
||||
"text-chat-disabled": !this.state.textChatEnabled,
|
||||
"text-chat-entries-empty": !messageList.length
|
||||
});
|
||||
|
||||
return (
|
||||
|
@ -946,6 +946,7 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
|
||||
var MediaLayoutView = React.createClass({displayName: "MediaLayoutView",
|
||||
propTypes: {
|
||||
children: React.PropTypes.node,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
displayScreenShare: React.PropTypes.bool.isRequired,
|
||||
isLocalLoading: React.PropTypes.bool.isRequired,
|
||||
@ -955,6 +956,9 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
localPosterUrl: React.PropTypes.string,
|
||||
localSrcVideoObject: React.PropTypes.object,
|
||||
localVideoMuted: React.PropTypes.bool.isRequired,
|
||||
// Passing in matchMedia, allows it to be overriden for ui-showcase's
|
||||
// benefit. We expect either the override or window.matchMedia.
|
||||
matchMedia: React.PropTypes.func.isRequired,
|
||||
remotePosterUrl: React.PropTypes.string,
|
||||
remoteSrcVideoObject: React.PropTypes.object,
|
||||
renderRemoteVideo: React.PropTypes.bool.isRequired,
|
||||
@ -964,6 +968,60 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
useDesktopPaths: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
isLocalMediaAbsolutelyPositioned: function(matchMedia) {
|
||||
if (!matchMedia) {
|
||||
matchMedia = this.props.matchMedia;
|
||||
}
|
||||
return matchMedia &&
|
||||
// The screen width is less than 640px and we are not screen sharing.
|
||||
((matchMedia("screen and (max-width:640px)").matches &&
|
||||
!this.props.displayScreenShare) ||
|
||||
// or the screen width is less than 300px.
|
||||
(matchMedia("screen and (max-width:300px)").matches));
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned()
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// This is all for the ui-showcase's benefit.
|
||||
if (this.props.matchMedia != nextProps.matchMedia) {
|
||||
this.updateLocalMediaState(null, nextProps.matchMedia);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener("resize", this.updateLocalMediaState);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.removeEventListener("resize", this.updateLocalMediaState);
|
||||
},
|
||||
|
||||
updateLocalMediaState: function(event, matchMedia) {
|
||||
var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia);
|
||||
if (this.state.localMediaAboslutelyPositioned != newState) {
|
||||
this.setState({
|
||||
localMediaAboslutelyPositioned: newState
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderLocalVideo: function() {
|
||||
return (
|
||||
React.createElement("div", {className: "local"},
|
||||
React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted,
|
||||
isLoading: this.props.isLocalLoading,
|
||||
mediaType: "local",
|
||||
posterUrl: this.props.localPosterUrl,
|
||||
srcVideoObject: this.props.localSrcVideoObject})
|
||||
)
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var remoteStreamClasses = React.addons.classSet({
|
||||
"remote": true,
|
||||
@ -979,7 +1037,9 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
"media-wrapper": true,
|
||||
"receiving-screen-share": this.props.displayScreenShare,
|
||||
"showing-local-streams": this.props.localSrcVideoObject ||
|
||||
this.props.localPosterUrl
|
||||
this.props.localPosterUrl,
|
||||
"showing-remote-streams": this.props.remoteSrcVideoObject ||
|
||||
this.props.remotePosterUrl || this.props.isRemoteLoading
|
||||
});
|
||||
|
||||
return (
|
||||
@ -993,7 +1053,10 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
isLoading: this.props.isRemoteLoading,
|
||||
mediaType: "remote",
|
||||
posterUrl: this.props.remotePosterUrl,
|
||||
srcVideoObject: this.props.remoteSrcVideoObject})
|
||||
srcVideoObject: this.props.remoteSrcVideoObject}),
|
||||
this.state.localMediaAboslutelyPositioned ?
|
||||
this.renderLocalVideo() : null,
|
||||
this.props.children
|
||||
),
|
||||
React.createElement("div", {className: screenShareStreamClasses},
|
||||
React.createElement(MediaView, {displayAvatar: false,
|
||||
@ -1006,13 +1069,8 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
dispatcher: this.props.dispatcher,
|
||||
showRoomName: this.props.showContextRoomName,
|
||||
useDesktopPaths: false}),
|
||||
React.createElement("div", {className: "local"},
|
||||
React.createElement(MediaView, {displayAvatar: this.props.localVideoMuted,
|
||||
isLoading: this.props.isLocalLoading,
|
||||
mediaType: "local",
|
||||
posterUrl: this.props.localPosterUrl,
|
||||
srcVideoObject: this.props.localSrcVideoObject})
|
||||
)
|
||||
this.state.localMediaAboslutelyPositioned ?
|
||||
null : this.renderLocalVideo()
|
||||
)
|
||||
)
|
||||
);
|
||||
|
@ -946,6 +946,7 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
|
||||
var MediaLayoutView = React.createClass({
|
||||
propTypes: {
|
||||
children: React.PropTypes.node,
|
||||
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
|
||||
displayScreenShare: React.PropTypes.bool.isRequired,
|
||||
isLocalLoading: React.PropTypes.bool.isRequired,
|
||||
@ -955,6 +956,9 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
localPosterUrl: React.PropTypes.string,
|
||||
localSrcVideoObject: React.PropTypes.object,
|
||||
localVideoMuted: React.PropTypes.bool.isRequired,
|
||||
// Passing in matchMedia, allows it to be overriden for ui-showcase's
|
||||
// benefit. We expect either the override or window.matchMedia.
|
||||
matchMedia: React.PropTypes.func.isRequired,
|
||||
remotePosterUrl: React.PropTypes.string,
|
||||
remoteSrcVideoObject: React.PropTypes.object,
|
||||
renderRemoteVideo: React.PropTypes.bool.isRequired,
|
||||
@ -964,6 +968,60 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
useDesktopPaths: React.PropTypes.bool.isRequired
|
||||
},
|
||||
|
||||
isLocalMediaAbsolutelyPositioned: function(matchMedia) {
|
||||
if (!matchMedia) {
|
||||
matchMedia = this.props.matchMedia;
|
||||
}
|
||||
return matchMedia &&
|
||||
// The screen width is less than 640px and we are not screen sharing.
|
||||
((matchMedia("screen and (max-width:640px)").matches &&
|
||||
!this.props.displayScreenShare) ||
|
||||
// or the screen width is less than 300px.
|
||||
(matchMedia("screen and (max-width:300px)").matches));
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
return {
|
||||
localMediaAboslutelyPositioned: this.isLocalMediaAbsolutelyPositioned()
|
||||
};
|
||||
},
|
||||
|
||||
componentWillReceiveProps: function(nextProps) {
|
||||
// This is all for the ui-showcase's benefit.
|
||||
if (this.props.matchMedia != nextProps.matchMedia) {
|
||||
this.updateLocalMediaState(null, nextProps.matchMedia);
|
||||
}
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
window.addEventListener("resize", this.updateLocalMediaState);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
window.removeEventListener("resize", this.updateLocalMediaState);
|
||||
},
|
||||
|
||||
updateLocalMediaState: function(event, matchMedia) {
|
||||
var newState = this.isLocalMediaAbsolutelyPositioned(matchMedia);
|
||||
if (this.state.localMediaAboslutelyPositioned != newState) {
|
||||
this.setState({
|
||||
localMediaAboslutelyPositioned: newState
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
renderLocalVideo: function() {
|
||||
return (
|
||||
<div className="local">
|
||||
<MediaView displayAvatar={this.props.localVideoMuted}
|
||||
isLoading={this.props.isLocalLoading}
|
||||
mediaType="local"
|
||||
posterUrl={this.props.localPosterUrl}
|
||||
srcVideoObject={this.props.localSrcVideoObject} />
|
||||
</div>
|
||||
);
|
||||
},
|
||||
|
||||
render: function() {
|
||||
var remoteStreamClasses = React.addons.classSet({
|
||||
"remote": true,
|
||||
@ -979,7 +1037,9 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
"media-wrapper": true,
|
||||
"receiving-screen-share": this.props.displayScreenShare,
|
||||
"showing-local-streams": this.props.localSrcVideoObject ||
|
||||
this.props.localPosterUrl
|
||||
this.props.localPosterUrl,
|
||||
"showing-remote-streams": this.props.remoteSrcVideoObject ||
|
||||
this.props.remotePosterUrl || this.props.isRemoteLoading
|
||||
});
|
||||
|
||||
return (
|
||||
@ -994,6 +1054,9 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
mediaType="remote"
|
||||
posterUrl={this.props.remotePosterUrl}
|
||||
srcVideoObject={this.props.remoteSrcVideoObject} />
|
||||
{ this.state.localMediaAboslutelyPositioned ?
|
||||
this.renderLocalVideo() : null }
|
||||
{ this.props.children }
|
||||
</div>
|
||||
<div className={screenShareStreamClasses}>
|
||||
<MediaView displayAvatar={false}
|
||||
@ -1006,13 +1069,8 @@ loop.shared.views = (function(_, mozL10n) {
|
||||
dispatcher={this.props.dispatcher}
|
||||
showRoomName={this.props.showContextRoomName}
|
||||
useDesktopPaths={false} />
|
||||
<div className="local">
|
||||
<MediaView displayAvatar={this.props.localVideoMuted}
|
||||
isLoading={this.props.isLocalLoading}
|
||||
mediaType="local"
|
||||
posterUrl={this.props.localPosterUrl}
|
||||
srcVideoObject={this.props.localSrcVideoObject} />
|
||||
</div>
|
||||
{ this.state.localMediaAboslutelyPositioned ?
|
||||
null : this.renderLocalVideo() }
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
|
@ -256,11 +256,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
mixins: [
|
||||
Backbone.Events,
|
||||
sharedMixins.MediaSetupMixin,
|
||||
sharedMixins.RoomsAudioMixin,
|
||||
loop.store.StoreMixin("activeRoomStore")
|
||||
sharedMixins.RoomsAudioMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
// We pass conversationStore here rather than use the mixin, to allow
|
||||
// easy configurability for the ui-showcase.
|
||||
activeRoomStore: React.PropTypes.oneOfType([
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
@ -282,6 +283,16 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.activeRoomStore.on("change", function() {
|
||||
this.setState(this.props.activeRoomStore.getStoreState());
|
||||
}, this);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.props.activeRoomStore.off("change", null, this);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Adding a class to the document body element from here to ease styling it.
|
||||
document.body.classList.add("is-standalone-room");
|
||||
@ -429,7 +440,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
*/
|
||||
_isScreenShareLoading: function() {
|
||||
return this.state.receivingScreenShare &&
|
||||
!this.state.screenShareVideoObject;
|
||||
!this.state.screenShareVideoObject &&
|
||||
!this.props.screenSharePosterUrl;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@ -456,6 +468,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
localPosterUrl: this.props.localPosterUrl,
|
||||
localSrcVideoObject: this.state.localSrcVideoObject,
|
||||
localVideoMuted: this.state.videoMuted,
|
||||
matchMedia: this.state.matchMedia || window.matchMedia.bind(window),
|
||||
remotePosterUrl: this.props.remotePosterUrl,
|
||||
remoteSrcVideoObject: this.state.remoteSrcVideoObject,
|
||||
renderRemoteVideo: this.shouldRenderRemoteVideo(),
|
||||
|
@ -256,11 +256,12 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
mixins: [
|
||||
Backbone.Events,
|
||||
sharedMixins.MediaSetupMixin,
|
||||
sharedMixins.RoomsAudioMixin,
|
||||
loop.store.StoreMixin("activeRoomStore")
|
||||
sharedMixins.RoomsAudioMixin
|
||||
],
|
||||
|
||||
propTypes: {
|
||||
// We pass conversationStore here rather than use the mixin, to allow
|
||||
// easy configurability for the ui-showcase.
|
||||
activeRoomStore: React.PropTypes.oneOfType([
|
||||
React.PropTypes.instanceOf(loop.store.ActiveRoomStore),
|
||||
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
|
||||
@ -282,6 +283,16 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
});
|
||||
},
|
||||
|
||||
componentWillMount: function() {
|
||||
this.props.activeRoomStore.on("change", function() {
|
||||
this.setState(this.props.activeRoomStore.getStoreState());
|
||||
}, this);
|
||||
},
|
||||
|
||||
componentWillUnmount: function() {
|
||||
this.props.activeRoomStore.off("change", null, this);
|
||||
},
|
||||
|
||||
componentDidMount: function() {
|
||||
// Adding a class to the document body element from here to ease styling it.
|
||||
document.body.classList.add("is-standalone-room");
|
||||
@ -429,7 +440,8 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
*/
|
||||
_isScreenShareLoading: function() {
|
||||
return this.state.receivingScreenShare &&
|
||||
!this.state.screenShareVideoObject;
|
||||
!this.state.screenShareVideoObject &&
|
||||
!this.props.screenSharePosterUrl;
|
||||
},
|
||||
|
||||
render: function() {
|
||||
@ -456,6 +468,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
|
||||
localPosterUrl={this.props.localPosterUrl}
|
||||
localSrcVideoObject={this.state.localSrcVideoObject}
|
||||
localVideoMuted={this.state.videoMuted}
|
||||
matchMedia={this.state.matchMedia || window.matchMedia.bind(window)}
|
||||
remotePosterUrl={this.props.remotePosterUrl}
|
||||
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
|
||||
renderRemoteVideo={this.shouldRenderRemoteVideo()}
|
||||
|
@ -474,7 +474,9 @@ describe("loop.conversationViews", function () {
|
||||
describe("OngoingConversationView", function() {
|
||||
function mountTestComponent(extraProps) {
|
||||
var props = _.extend({
|
||||
dispatcher: dispatcher
|
||||
conversationStore: conversationStore,
|
||||
dispatcher: dispatcher,
|
||||
matchMedia: window.matchMedia
|
||||
}, extraProps);
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(loop.conversationViews.OngoingConversationView, props));
|
||||
@ -489,15 +491,6 @@ describe("loop.conversationViews", function () {
|
||||
sinon.match.hasOwn("name", "setupStreamElements"));
|
||||
});
|
||||
|
||||
it("should display an avatar for remote video when the stream is not enabled", function() {
|
||||
view = mountTestComponent({
|
||||
mediaConnected: true,
|
||||
remoteVideoEnabled: false
|
||||
});
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
|
||||
});
|
||||
|
||||
it("should display the remote video when the stream is enabled", function() {
|
||||
conversationStore.setStoreState({
|
||||
remoteSrcVideoObject: { fake: 1 }
|
||||
@ -511,16 +504,6 @@ describe("loop.conversationViews", function () {
|
||||
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
|
||||
});
|
||||
|
||||
it("should display an avatar for local video when the stream is not enabled", function() {
|
||||
view = mountTestComponent({
|
||||
video: {
|
||||
enabled: false
|
||||
}
|
||||
});
|
||||
|
||||
TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
|
||||
});
|
||||
|
||||
it("should display the local video when the stream is enabled", function() {
|
||||
conversationStore.setStoreState({
|
||||
localSrcVideoObject: { fake: 1 }
|
||||
|
@ -95,7 +95,7 @@
|
||||
|
||||
describe("Unexpected Warnings Check", function() {
|
||||
it("should long only the warnings we expect", function() {
|
||||
chai.expect(caughtWarnings.length).to.eql(27);
|
||||
chai.expect(caughtWarnings.length).to.eql(28);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -598,21 +598,6 @@ describe("loop.roomViews", function () {
|
||||
|
||||
});
|
||||
|
||||
describe("Mute", function() {
|
||||
it("should render local media as audio-only if video is muted",
|
||||
function() {
|
||||
activeRoomStore.setStoreState({
|
||||
roomState: ROOM_STATES.SESSION_CONNECTED,
|
||||
videoMuted: true
|
||||
});
|
||||
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(view.getDOMNode().querySelector(".local-stream-audio"))
|
||||
.not.eql(null);
|
||||
});
|
||||
});
|
||||
|
||||
describe("Edit Context", function() {
|
||||
it("should show the form when the edit button is clicked", function() {
|
||||
view = mountTestComponent();
|
||||
|
@ -99,7 +99,7 @@ class Test1BrowserCall(MarionetteTestCase):
|
||||
self.switch_to_chatbox()
|
||||
|
||||
# expect a video container on desktop side
|
||||
media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media")
|
||||
media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media-layout")
|
||||
self.assertEqual(media_container.tag_name, "div", "expect a video container")
|
||||
|
||||
self.check_video(".local-video")
|
||||
|
@ -21,6 +21,7 @@
|
||||
"gMozLoopAPI": true,
|
||||
"mockDb": true,
|
||||
"mockPushHandler": true,
|
||||
"OpenBrowserWindow": true,
|
||||
"promiseDeletedOAuthParams": false,
|
||||
"promiseOAuthGetRegistration": false,
|
||||
"promiseOAuthParamsSetup": false,
|
||||
|
@ -167,3 +167,19 @@ add_task(function* test_screen_share() {
|
||||
MozLoopService.setScreenShareState("1", false);
|
||||
Assert.strictEqual(LoopUI.toolbarButton.node.getAttribute("state"), "", "Check button is in default state");
|
||||
});
|
||||
|
||||
add_task(function* test_private_browsing_window() {
|
||||
let win = OpenBrowserWindow({ private: true });
|
||||
yield new Promise(resolve => {
|
||||
win.addEventListener("load", function listener() {
|
||||
win.removeEventListener("load", listener);
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
|
||||
let button = win.LoopUI.toolbarButton.node;
|
||||
Assert.ok(button, "Loop button should be present");
|
||||
Assert.ok(button.getAttribute("disabled"), "Disabled attribute should be set");
|
||||
|
||||
win.close();
|
||||
});
|
||||
|
@ -916,26 +916,6 @@ describe("loop.store.ActiveRoomStore", function () {
|
||||
});
|
||||
});
|
||||
|
||||
it("should retry publishing if on desktop, and in the videoMuted state", function() {
|
||||
store._isDesktop = true;
|
||||
|
||||
store.connectionFailure(new sharedActions.ConnectionFailure({
|
||||
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(fakeSdkDriver.retryPublishWithoutVideo);
|
||||
});
|
||||
|
||||
it("should set videoMuted to try when retrying publishing", function() {
|
||||
store._isDesktop = true;
|
||||
|
||||
store.connectionFailure(new sharedActions.ConnectionFailure({
|
||||
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
|
||||
}));
|
||||
|
||||
expect(store.getStoreState().videoMuted).eql(true);
|
||||
});
|
||||
|
||||
it("should store the failure reason", function() {
|
||||
store.connectionFailure(connectionFailureAction);
|
||||
|
||||
|
@ -147,26 +147,6 @@ describe("loop.store.ConversationStore", function () {
|
||||
store.setStoreState({windowId: "42"});
|
||||
});
|
||||
|
||||
it("should retry publishing if on desktop, and in the videoMuted state", function() {
|
||||
store._isDesktop = true;
|
||||
|
||||
store.connectionFailure(new sharedActions.ConnectionFailure({
|
||||
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
|
||||
}));
|
||||
|
||||
sinon.assert.calledOnce(sdkDriver.retryPublishWithoutVideo);
|
||||
});
|
||||
|
||||
it("should set videoMuted to try when retrying publishing", function() {
|
||||
store._isDesktop = true;
|
||||
|
||||
store.connectionFailure(new sharedActions.ConnectionFailure({
|
||||
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
|
||||
}));
|
||||
|
||||
expect(store.getStoreState().videoMuted).eql(true);
|
||||
});
|
||||
|
||||
it("should disconnect the session", function() {
|
||||
store.connectionFailure(
|
||||
new sharedActions.ConnectionFailure({reason: "fake"}));
|
||||
|
@ -133,43 +133,6 @@ describe("loop.OTSdkDriver", function () {
|
||||
});
|
||||
});
|
||||
|
||||
describe("#retryPublishWithoutVideo", function() {
|
||||
beforeEach(function() {
|
||||
sdk.initPublisher.returns(publisher);
|
||||
|
||||
driver.setupStreamElements(new sharedActions.SetupStreamElements({
|
||||
publisherConfig: publisherConfig
|
||||
}));
|
||||
});
|
||||
|
||||
it("should make MediaStreamTrack.getSources return without a video source", function(done) {
|
||||
driver.retryPublishWithoutVideo();
|
||||
|
||||
window.MediaStreamTrack.getSources(function(sources) {
|
||||
expect(sources.some(function(src) {
|
||||
return src.kind === "video";
|
||||
})).eql(false);
|
||||
|
||||
done();
|
||||
});
|
||||
});
|
||||
|
||||
it("should call initPublisher", function() {
|
||||
driver.retryPublishWithoutVideo();
|
||||
|
||||
var expectedConfig = _.extend({
|
||||
channels: {
|
||||
text: {}
|
||||
}
|
||||
}, publisherConfig);
|
||||
|
||||
sinon.assert.calledTwice(sdk.initPublisher);
|
||||
sinon.assert.calledWith(sdk.initPublisher,
|
||||
sinon.match.instanceOf(HTMLDivElement),
|
||||
expectedConfig);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#setMute", function() {
|
||||
beforeEach(function() {
|
||||
sdk.initPublisher.returns(publisher);
|
||||
|
@ -56,27 +56,6 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
store.setStoreState({ textChatEnabled: true });
|
||||
});
|
||||
|
||||
it("should add an empty class when the list is empty", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: []
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true);
|
||||
});
|
||||
|
||||
it("should not add an empty class when the list is has items", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
type: CHAT_MESSAGE_TYPES.RECEIVED,
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!",
|
||||
receivedTimestamp: "2015-06-25T17:53:55.357Z"
|
||||
}]
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
|
||||
});
|
||||
|
||||
it("should render message entries when message were sent/ received", function() {
|
||||
view = mountTestComponent({
|
||||
messageList: [{
|
||||
@ -297,6 +276,41 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
fakeServer.restore();
|
||||
});
|
||||
|
||||
it("should add a disabled class when text chat is disabled", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
store.setStoreState({ textChatEnabled: false });
|
||||
|
||||
expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(true);
|
||||
});
|
||||
|
||||
it("should not a disabled class when text chat is enabled", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
store.setStoreState({ textChatEnabled: true });
|
||||
|
||||
expect(view.getDOMNode().classList.contains("text-chat-disabled")).eql(false);
|
||||
});
|
||||
|
||||
it("should add an empty class when the entries list is empty", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(true);
|
||||
});
|
||||
|
||||
it("should not add an empty class when the entries list is has items", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
store.sendTextChatMessage({
|
||||
contentType: CHAT_CONTENT_TYPES.TEXT,
|
||||
message: "Hello!",
|
||||
sentTimestamp: "1970-01-01T00:02:00.000Z",
|
||||
receivedTimestamp: "1970-01-01T00:02:00.000Z"
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().classList.contains("text-chat-entries-empty")).eql(false);
|
||||
});
|
||||
|
||||
it("should show timestamps from msgs sent more than 1 min apart", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
@ -326,12 +340,6 @@ describe("loop.shared.views.TextChatView", function () {
|
||||
.to.eql(2);
|
||||
});
|
||||
|
||||
it("should display the view if no messages and text chat is enabled", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
expect(view.getDOMNode()).not.eql(null);
|
||||
});
|
||||
|
||||
it("should render message entries when message were sent/ received", function() {
|
||||
view = mountTestComponent();
|
||||
|
||||
|
@ -1057,6 +1057,7 @@ describe("loop.shared.views", function() {
|
||||
isRemoteLoading: false,
|
||||
isScreenShareLoading: false,
|
||||
localVideoMuted: false,
|
||||
matchMedia: window.matchMedia,
|
||||
renderRemoteVideo: false,
|
||||
showContextRoomName: false,
|
||||
useDesktopPaths: false
|
||||
@ -1144,5 +1145,35 @@ describe("loop.shared.views", function() {
|
||||
expect(view.getDOMNode().querySelector(".media-wrapper")
|
||||
.classList.contains("showing-local-streams")).eql(true);
|
||||
});
|
||||
|
||||
it("should not mark the wrapper as showing remote streams when not displaying a stream", function() {
|
||||
view = mountTestComponent({
|
||||
remoteSrcVideoObject: null,
|
||||
remotePosterUrl: null
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".media-wrapper")
|
||||
.classList.contains("showing-remote-streams")).eql(false);
|
||||
});
|
||||
|
||||
it("should mark the wrapper as showing remote streams when displaying a stream", function() {
|
||||
view = mountTestComponent({
|
||||
remoteSrcVideoObject: {},
|
||||
remotePosterUrl: null
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".media-wrapper")
|
||||
.classList.contains("showing-remote-streams")).eql(true);
|
||||
});
|
||||
|
||||
it("should mark the wrapper as showing remote streams when displaying a poster url", function() {
|
||||
view = mountTestComponent({
|
||||
remoteSrcVideoObject: {},
|
||||
remotePosterUrl: "fake/url"
|
||||
});
|
||||
|
||||
expect(view.getDOMNode().querySelector(".media-wrapper")
|
||||
.classList.contains("showing-remote-streams")).eql(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -75,13 +75,25 @@
|
||||
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
|
||||
var mockSDK = _.extend({
|
||||
var MockSDK = function() {
|
||||
dispatcher.register(this, [
|
||||
"setupStreamElements"
|
||||
]);
|
||||
};
|
||||
|
||||
MockSDK.prototype = {
|
||||
setupStreamElements: function() {
|
||||
// Dummy function to stop warnings.
|
||||
},
|
||||
|
||||
sendTextChatMessage: function(message) {
|
||||
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
|
||||
message: message.message
|
||||
}));
|
||||
}
|
||||
}, Backbone.Events);
|
||||
};
|
||||
|
||||
var mockSDK = new MockSDK();
|
||||
|
||||
/**
|
||||
* Every view that uses an activeRoomStore needs its own; if they shared
|
||||
@ -116,7 +128,6 @@
|
||||
});
|
||||
|
||||
store.forcedUpdate = function forcedUpdate(contentWindow) {
|
||||
|
||||
// Since this is called by setTimeout, we don't want to lose any
|
||||
// exceptions if there's a problem and we need to debug, so...
|
||||
try {
|
||||
@ -136,6 +147,17 @@
|
||||
camera: {height: 480, orientation: 0, width: 640}
|
||||
},
|
||||
remoteVideoEnabled: options.remoteVideoEnabled,
|
||||
// Override the matchMedia, this is so that the correct version is
|
||||
// used for the frame.
|
||||
//
|
||||
// Currently, we use an icky hack, and the showcase conspires with
|
||||
// react-frame-component to set iframe.contentWindow.matchMedia onto
|
||||
// the store. Once React context matures a bit (somewhere between
|
||||
// 0.14 and 1.0, apparently):
|
||||
//
|
||||
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
|
||||
//
|
||||
// we should be able to use those to clean this up.
|
||||
matchMedia: contentWindow.matchMedia.bind(contentWindow),
|
||||
roomState: options.roomState,
|
||||
videoMuted: !!options.videoMuted
|
||||
@ -185,6 +207,10 @@
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var updatingMobileActiveRoomStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var localFaceMuteRoomStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
videoMuted: true
|
||||
@ -201,12 +227,19 @@
|
||||
receivingScreenShare: true
|
||||
});
|
||||
|
||||
var updatingSharingRoomMobileStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
receivingScreenShare: true
|
||||
});
|
||||
|
||||
var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
|
||||
mediaConnected: false,
|
||||
receivingScreenShare: true,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
remoteSrcVideoObject: false
|
||||
});
|
||||
var loadingScreenSharingRoomStore = makeActiveRoomStore({
|
||||
receivingScreenShare: true,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
@ -234,7 +267,10 @@
|
||||
});
|
||||
|
||||
var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop
|
||||
mozLoop: navigator.mozLoop,
|
||||
activeRoomStore: makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.INIT
|
||||
})
|
||||
});
|
||||
|
||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
@ -253,6 +289,20 @@
|
||||
})
|
||||
});
|
||||
|
||||
var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
activeRoomStore: makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
})
|
||||
});
|
||||
|
||||
var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
activeRoomStore: makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
})
|
||||
});
|
||||
|
||||
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
videoMuted: true
|
||||
@ -272,15 +322,59 @@
|
||||
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
|
||||
});
|
||||
|
||||
var conversationStore = new loop.store.ConversationStore(dispatcher, {
|
||||
client: {},
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
var textChatStore = new loop.store.TextChatStore(dispatcher, {
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
|
||||
/**
|
||||
* Every view that uses an conversationStore needs its own; if they shared
|
||||
* a conversation store, they'd interfere with each other.
|
||||
*
|
||||
* @param options
|
||||
* @returns {loop.store.ConversationStore}
|
||||
*/
|
||||
function makeConversationStore() {
|
||||
var roomDispatcher = new loop.Dispatcher();
|
||||
|
||||
var store = new loop.store.ConversationStore(dispatcher, {
|
||||
client: {},
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
|
||||
store.forcedUpdate = function forcedUpdate(contentWindow) {
|
||||
// Since this is called by setTimeout, we don't want to lose any
|
||||
// exceptions if there's a problem and we need to debug, so...
|
||||
try {
|
||||
var newStoreState = {
|
||||
// Override the matchMedia, this is so that the correct version is
|
||||
// used for the frame.
|
||||
//
|
||||
// Currently, we use an icky hack, and the showcase conspires with
|
||||
// react-frame-component to set iframe.contentWindow.matchMedia onto
|
||||
// the store. Once React context matures a bit (somewhere between
|
||||
// 0.14 and 1.0, apparently):
|
||||
//
|
||||
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
|
||||
//
|
||||
// we should be able to use those to clean this up.
|
||||
matchMedia: contentWindow.matchMedia.bind(contentWindow)
|
||||
};
|
||||
|
||||
store.setStoreState(newStoreState);
|
||||
} catch (ex) {
|
||||
console.error("exception in forcedUpdate:", ex);
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
var conversationStores = [];
|
||||
for (var index = 0; index < 5; index++) {
|
||||
conversationStores[index] = makeConversationStore();
|
||||
}
|
||||
|
||||
// Update the text chat store with the room info.
|
||||
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
|
||||
roomName: "A Very Long Conversation Name",
|
||||
@ -341,7 +435,7 @@
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
activeRoomStore: activeRoomStore,
|
||||
conversationStore: conversationStore,
|
||||
conversationStore: conversationStores[0],
|
||||
textChatStore: textChatStore
|
||||
});
|
||||
|
||||
@ -360,14 +454,6 @@
|
||||
requestCallUrlInfo: noop
|
||||
};
|
||||
|
||||
var mockConversationModel = new loop.shared.models.ConversationModel({
|
||||
callerId: "Mrs Jones",
|
||||
urlCreationDate: (new Date() / 1000).toString()
|
||||
}, {
|
||||
sdk: mockSDK
|
||||
});
|
||||
mockConversationModel.startSession = noop;
|
||||
|
||||
var mockWebSocket = new loop.CallConnectionWebSocket({
|
||||
url: "fake",
|
||||
callId: "fakeId",
|
||||
@ -763,7 +849,7 @@
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(CallFailedView, {dispatcher: dispatcher,
|
||||
outgoing: false,
|
||||
store: conversationStore})
|
||||
store: conversationStores[0]})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {dashed: true,
|
||||
@ -772,7 +858,7 @@
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(CallFailedView, {dispatcher: dispatcher,
|
||||
outgoing: true,
|
||||
store: conversationStore})
|
||||
store: conversationStores[1]})
|
||||
)
|
||||
),
|
||||
React.createElement(Example, {dashed: true,
|
||||
@ -781,18 +867,22 @@
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(CallFailedView, {dispatcher: dispatcher, emailLinkError: true,
|
||||
outgoing: true,
|
||||
store: conversationStore})
|
||||
store: conversationStores[0]})
|
||||
)
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(Section, {name: "OngoingConversationView"},
|
||||
React.createElement(FramedExample, {height: 254,
|
||||
summary: "Desktop ongoing conversation window",
|
||||
width: 298},
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 394,
|
||||
onContentsRendered: conversationStores[0].forcedUpdate,
|
||||
summary: "Desktop ongoing conversation window",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: true},
|
||||
conversationStore: conversationStores[0],
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mediaConnected: true,
|
||||
@ -802,28 +892,55 @@
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {height: 600,
|
||||
summary: "Desktop ongoing conversation window large",
|
||||
width: 800},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: true},
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mediaConnected: true,
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
remoteVideoEnabled: true,
|
||||
video: {enabled: true}})
|
||||
)
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 400,
|
||||
onContentsRendered: conversationStores[1].forcedUpdate,
|
||||
summary: "Desktop ongoing conversation window (medium)",
|
||||
width: 600},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: true},
|
||||
conversationStore: conversationStores[1],
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mediaConnected: true,
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
remoteVideoEnabled: true,
|
||||
video: {enabled: true}})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {height: 254,
|
||||
React.createElement(FramedExample, {
|
||||
height: 600,
|
||||
onContentsRendered: conversationStores[2].forcedUpdate,
|
||||
summary: "Desktop ongoing conversation window (large)",
|
||||
width: 800},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: true},
|
||||
conversationStore: conversationStores[2],
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mediaConnected: true,
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
remoteVideoEnabled: true,
|
||||
video: {enabled: true}})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 394,
|
||||
onContentsRendered: conversationStores[3].forcedUpdate,
|
||||
summary: "Desktop ongoing conversation window - local face mute",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: true},
|
||||
conversationStore: conversationStores[3],
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mediaConnected: true,
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
remoteVideoEnabled: true,
|
||||
@ -831,15 +948,19 @@
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {height: 254,
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true, height: 394,
|
||||
onContentsRendered: conversationStores[4].forcedUpdate,
|
||||
summary: "Desktop ongoing conversation window - remote face mute",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(OngoingConversationView, {
|
||||
audio: {enabled: true},
|
||||
conversationStore: conversationStores[4],
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mediaConnected: true,
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
remoteVideoEnabled: false,
|
||||
video: {enabled: true}})
|
||||
)
|
||||
@ -894,7 +1015,8 @@
|
||||
|
||||
React.createElement(Section, {name: "DesktopRoomConversationView"},
|
||||
React.createElement(FramedExample, {
|
||||
height: 254,
|
||||
height: 398,
|
||||
onContentsRendered: invitationRoomStore.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
@ -911,6 +1033,7 @@
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 394,
|
||||
onContentsRendered: desktopRoomStoreLoading.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation (loading)",
|
||||
width: 298},
|
||||
/* Hide scrollbars here. Rotating loading div overflows and causes
|
||||
@ -927,8 +1050,12 @@
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {height: 254,
|
||||
summary: "Desktop room conversation"},
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 394,
|
||||
onContentsRendered: roomStore.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
@ -941,10 +1068,48 @@
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {dashed: true,
|
||||
height: 394,
|
||||
summary: "Desktop room conversation local face-mute",
|
||||
width: 298},
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 482,
|
||||
onContentsRendered: desktopRoomStoreMedium.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation (medium)",
|
||||
width: 602},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mozLoop: navigator.mozLoop,
|
||||
onCallTerminated: function(){},
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
roomStore: desktopRoomStoreMedium})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 485,
|
||||
onContentsRendered: desktopRoomStoreLarge.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation (large)",
|
||||
width: 646},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mozLoop: navigator.mozLoop,
|
||||
onCallTerminated: function(){},
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
roomStore: desktopRoomStoreLarge})
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {
|
||||
dashed: true,
|
||||
height: 394,
|
||||
onContentsRendered: desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation local face-mute",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(DesktopRoomConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
@ -955,7 +1120,9 @@
|
||||
)
|
||||
),
|
||||
|
||||
React.createElement(FramedExample, {dashed: true, height: 394,
|
||||
React.createElement(FramedExample, {dashed: true,
|
||||
height: 394,
|
||||
onContentsRendered: desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate,
|
||||
summary: "Desktop room conversation remote face-mute",
|
||||
width: 298},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
@ -964,6 +1131,7 @@
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
mozLoop: navigator.mozLoop,
|
||||
onCallTerminated: function(){},
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
roomStore: desktopRemoteFaceMuteRoomStore})
|
||||
)
|
||||
)
|
||||
@ -1081,8 +1249,7 @@
|
||||
isFirefox: true,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
screenSharePosterUrl: "sample-img/video-screen-baz.png"})
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS})
|
||||
)
|
||||
),
|
||||
|
||||
@ -1102,8 +1269,7 @@
|
||||
isFirefox: true,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
remotePosterUrl: "sample-img/video-screen-remote.png",
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
screenSharePosterUrl: "sample-img/video-screen-baz.png"})
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS})
|
||||
)
|
||||
),
|
||||
|
||||
@ -1171,12 +1337,12 @@
|
||||
cssClass: "standalone",
|
||||
dashed: true,
|
||||
height: 480,
|
||||
onContentsRendered: updatingActiveRoomStore.forcedUpdate,
|
||||
onContentsRendered: updatingMobileActiveRoomStore.forcedUpdate,
|
||||
summary: "Standalone room conversation (has-participants, 600x480)",
|
||||
width: 600},
|
||||
React.createElement("div", {className: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
activeRoomStore: updatingActiveRoomStore,
|
||||
activeRoomStore: updatingMobileActiveRoomStore,
|
||||
dispatcher: dispatcher,
|
||||
isFirefox: true,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
@ -1189,12 +1355,12 @@
|
||||
cssClass: "standalone",
|
||||
dashed: true,
|
||||
height: 480,
|
||||
onContentsRendered: updatingSharingRoomStore.forcedUpdate,
|
||||
onContentsRendered: updatingSharingRoomMobileStore.forcedUpdate,
|
||||
summary: "Standalone room convo (has-participants, receivingScreenShare, 600x480)",
|
||||
width: 600},
|
||||
React.createElement("div", {className: "standalone", cssClass: "standalone"},
|
||||
React.createElement(StandaloneRoomView, {
|
||||
activeRoomStore: updatingSharingRoomStore,
|
||||
activeRoomStore: updatingSharingRoomMobileStore,
|
||||
dispatcher: dispatcher,
|
||||
isFirefox: true,
|
||||
localPosterUrl: "sample-img/video-screen-local.png",
|
||||
@ -1282,7 +1448,7 @@
|
||||
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
var expectedWarningsCount = 23;
|
||||
var expectedWarningsCount = 18;
|
||||
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
|
||||
if (uncaughtError || warningsMismatch) {
|
||||
$("#results").append("<div class='failures'><em>" +
|
||||
|
@ -75,13 +75,25 @@
|
||||
|
||||
var dispatcher = new loop.Dispatcher();
|
||||
|
||||
var mockSDK = _.extend({
|
||||
var MockSDK = function() {
|
||||
dispatcher.register(this, [
|
||||
"setupStreamElements"
|
||||
]);
|
||||
};
|
||||
|
||||
MockSDK.prototype = {
|
||||
setupStreamElements: function() {
|
||||
// Dummy function to stop warnings.
|
||||
},
|
||||
|
||||
sendTextChatMessage: function(message) {
|
||||
dispatcher.dispatch(new loop.shared.actions.ReceivedTextChatMessage({
|
||||
message: message.message
|
||||
}));
|
||||
}
|
||||
}, Backbone.Events);
|
||||
};
|
||||
|
||||
var mockSDK = new MockSDK();
|
||||
|
||||
/**
|
||||
* Every view that uses an activeRoomStore needs its own; if they shared
|
||||
@ -116,7 +128,6 @@
|
||||
});
|
||||
|
||||
store.forcedUpdate = function forcedUpdate(contentWindow) {
|
||||
|
||||
// Since this is called by setTimeout, we don't want to lose any
|
||||
// exceptions if there's a problem and we need to debug, so...
|
||||
try {
|
||||
@ -136,6 +147,17 @@
|
||||
camera: {height: 480, orientation: 0, width: 640}
|
||||
},
|
||||
remoteVideoEnabled: options.remoteVideoEnabled,
|
||||
// Override the matchMedia, this is so that the correct version is
|
||||
// used for the frame.
|
||||
//
|
||||
// Currently, we use an icky hack, and the showcase conspires with
|
||||
// react-frame-component to set iframe.contentWindow.matchMedia onto
|
||||
// the store. Once React context matures a bit (somewhere between
|
||||
// 0.14 and 1.0, apparently):
|
||||
//
|
||||
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
|
||||
//
|
||||
// we should be able to use those to clean this up.
|
||||
matchMedia: contentWindow.matchMedia.bind(contentWindow),
|
||||
roomState: options.roomState,
|
||||
videoMuted: !!options.videoMuted
|
||||
@ -185,6 +207,10 @@
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var updatingMobileActiveRoomStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
var localFaceMuteRoomStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
videoMuted: true
|
||||
@ -201,12 +227,19 @@
|
||||
receivingScreenShare: true
|
||||
});
|
||||
|
||||
var updatingSharingRoomMobileStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
receivingScreenShare: true
|
||||
});
|
||||
|
||||
var loadingRemoteLoadingScreenStore = makeActiveRoomStore({
|
||||
mediaConnected: false,
|
||||
receivingScreenShare: true,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
remoteSrcVideoObject: false
|
||||
});
|
||||
var loadingScreenSharingRoomStore = makeActiveRoomStore({
|
||||
receivingScreenShare: true,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
});
|
||||
|
||||
@ -234,7 +267,10 @@
|
||||
});
|
||||
|
||||
var invitationRoomStore = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop
|
||||
mozLoop: navigator.mozLoop,
|
||||
activeRoomStore: makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.INIT
|
||||
})
|
||||
});
|
||||
|
||||
var roomStore = new loop.store.RoomStore(dispatcher, {
|
||||
@ -253,6 +289,20 @@
|
||||
})
|
||||
});
|
||||
|
||||
var desktopRoomStoreMedium = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
activeRoomStore: makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
})
|
||||
});
|
||||
|
||||
var desktopRoomStoreLarge = new loop.store.RoomStore(dispatcher, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
activeRoomStore: makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS
|
||||
})
|
||||
});
|
||||
|
||||
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
videoMuted: true
|
||||
@ -272,15 +322,59 @@
|
||||
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
|
||||
});
|
||||
|
||||
var conversationStore = new loop.store.ConversationStore(dispatcher, {
|
||||
client: {},
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
var textChatStore = new loop.store.TextChatStore(dispatcher, {
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
|
||||
/**
|
||||
* Every view that uses an conversationStore needs its own; if they shared
|
||||
* a conversation store, they'd interfere with each other.
|
||||
*
|
||||
* @param options
|
||||
* @returns {loop.store.ConversationStore}
|
||||
*/
|
||||
function makeConversationStore() {
|
||||
var roomDispatcher = new loop.Dispatcher();
|
||||
|
||||
var store = new loop.store.ConversationStore(dispatcher, {
|
||||
client: {},
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
});
|
||||
|
||||
store.forcedUpdate = function forcedUpdate(contentWindow) {
|
||||
// Since this is called by setTimeout, we don't want to lose any
|
||||
// exceptions if there's a problem and we need to debug, so...
|
||||
try {
|
||||
var newStoreState = {
|
||||
// Override the matchMedia, this is so that the correct version is
|
||||
// used for the frame.
|
||||
//
|
||||
// Currently, we use an icky hack, and the showcase conspires with
|
||||
// react-frame-component to set iframe.contentWindow.matchMedia onto
|
||||
// the store. Once React context matures a bit (somewhere between
|
||||
// 0.14 and 1.0, apparently):
|
||||
//
|
||||
// https://facebook.github.io/react/blog/2015/02/24/streamlining-react-elements.html#solution-make-context-parent-based-instead-of-owner-based
|
||||
//
|
||||
// we should be able to use those to clean this up.
|
||||
matchMedia: contentWindow.matchMedia.bind(contentWindow)
|
||||
};
|
||||
|
||||
store.setStoreState(newStoreState);
|
||||
} catch (ex) {
|
||||
console.error("exception in forcedUpdate:", ex);
|
||||
}
|
||||
};
|
||||
|
||||
return store;
|
||||
}
|
||||
|
||||
var conversationStores = [];
|
||||
for (var index = 0; index < 5; index++) {
|
||||
conversationStores[index] = makeConversationStore();
|
||||
}
|
||||
|
||||
// Update the text chat store with the room info.
|
||||
textChatStore.updateRoomInfo(new sharedActions.UpdateRoomInfo({
|
||||
roomName: "A Very Long Conversation Name",
|
||||
@ -341,7 +435,7 @@
|
||||
|
||||
loop.store.StoreMixin.register({
|
||||
activeRoomStore: activeRoomStore,
|
||||
conversationStore: conversationStore,
|
||||
conversationStore: conversationStores[0],
|
||||
textChatStore: textChatStore
|
||||
});
|
||||
|
||||
@ -360,14 +454,6 @@
|
||||
requestCallUrlInfo: noop
|
||||
};
|
||||
|
||||
var mockConversationModel = new loop.shared.models.ConversationModel({
|
||||
callerId: "Mrs Jones",
|
||||
urlCreationDate: (new Date() / 1000).toString()
|
||||
}, {
|
||||
sdk: mockSDK
|
||||
});
|
||||
mockConversationModel.startSession = noop;
|
||||
|
||||
var mockWebSocket = new loop.CallConnectionWebSocket({
|
||||
url: "fake",
|
||||
callId: "fakeId",
|
||||
@ -763,7 +849,7 @@
|
||||
<div className="fx-embedded">
|
||||
<CallFailedView dispatcher={dispatcher}
|
||||
outgoing={false}
|
||||
store={conversationStore} />
|
||||
store={conversationStores[0]} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example dashed={true}
|
||||
@ -772,7 +858,7 @@
|
||||
<div className="fx-embedded">
|
||||
<CallFailedView dispatcher={dispatcher}
|
||||
outgoing={true}
|
||||
store={conversationStore} />
|
||||
store={conversationStores[1]} />
|
||||
</div>
|
||||
</Example>
|
||||
<Example dashed={true}
|
||||
@ -781,18 +867,22 @@
|
||||
<div className="fx-embedded">
|
||||
<CallFailedView dispatcher={dispatcher} emailLinkError={true}
|
||||
outgoing={true}
|
||||
store={conversationStore} />
|
||||
store={conversationStores[0]} />
|
||||
</div>
|
||||
</Example>
|
||||
</Section>
|
||||
|
||||
<Section name="OngoingConversationView">
|
||||
<FramedExample height={254}
|
||||
summary="Desktop ongoing conversation window"
|
||||
width={298}>
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={394}
|
||||
onContentsRendered={conversationStores[0].forcedUpdate}
|
||||
summary="Desktop ongoing conversation window"
|
||||
width={298}>
|
||||
<div className="fx-embedded">
|
||||
<OngoingConversationView
|
||||
audio={{enabled: true}}
|
||||
conversationStore={conversationStores[0]}
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mediaConnected={true}
|
||||
@ -802,28 +892,55 @@
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample height={600}
|
||||
summary="Desktop ongoing conversation window large"
|
||||
width={800}>
|
||||
<div className="fx-embedded">
|
||||
<OngoingConversationView
|
||||
audio={{enabled: true}}
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mediaConnected={true}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
remoteVideoEnabled={true}
|
||||
video={{enabled: true}} />
|
||||
</div>
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={400}
|
||||
onContentsRendered={conversationStores[1].forcedUpdate}
|
||||
summary="Desktop ongoing conversation window (medium)"
|
||||
width={600}>
|
||||
<div className="fx-embedded">
|
||||
<OngoingConversationView
|
||||
audio={{enabled: true}}
|
||||
conversationStore={conversationStores[1]}
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mediaConnected={true}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
remoteVideoEnabled={true}
|
||||
video={{enabled: true}} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample height={254}
|
||||
<FramedExample
|
||||
height={600}
|
||||
onContentsRendered={conversationStores[2].forcedUpdate}
|
||||
summary="Desktop ongoing conversation window (large)"
|
||||
width={800}>
|
||||
<div className="fx-embedded">
|
||||
<OngoingConversationView
|
||||
audio={{enabled: true}}
|
||||
conversationStore={conversationStores[2]}
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mediaConnected={true}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
remoteVideoEnabled={true}
|
||||
video={{enabled: true}} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={394}
|
||||
onContentsRendered={conversationStores[3].forcedUpdate}
|
||||
summary="Desktop ongoing conversation window - local face mute"
|
||||
width={298} >
|
||||
<div className="fx-embedded">
|
||||
<OngoingConversationView
|
||||
audio={{enabled: true}}
|
||||
conversationStore={conversationStores[3]}
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mediaConnected={true}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
remoteVideoEnabled={true}
|
||||
@ -831,15 +948,19 @@
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample height={254}
|
||||
<FramedExample
|
||||
dashed={true} height={394}
|
||||
onContentsRendered={conversationStores[4].forcedUpdate}
|
||||
summary="Desktop ongoing conversation window - remote face mute"
|
||||
width={298} >
|
||||
<div className="fx-embedded">
|
||||
<OngoingConversationView
|
||||
audio={{enabled: true}}
|
||||
conversationStore={conversationStores[4]}
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mediaConnected={true}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
remoteVideoEnabled={false}
|
||||
video={{enabled: true}} />
|
||||
</div>
|
||||
@ -894,7 +1015,8 @@
|
||||
|
||||
<Section name="DesktopRoomConversationView">
|
||||
<FramedExample
|
||||
height={254}
|
||||
height={398}
|
||||
onContentsRendered={invitationRoomStore.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation (invitation, text-chat inclusion/scrollbars don't happen in real client)"
|
||||
width={298}>
|
||||
<div className="fx-embedded">
|
||||
@ -911,6 +1033,7 @@
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={394}
|
||||
onContentsRendered={desktopRoomStoreLoading.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation (loading)"
|
||||
width={298}>
|
||||
{/* Hide scrollbars here. Rotating loading div overflows and causes
|
||||
@ -927,8 +1050,12 @@
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample height={254}
|
||||
summary="Desktop room conversation">
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={394}
|
||||
onContentsRendered={roomStore.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation"
|
||||
width={298}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopRoomConversationView
|
||||
dispatcher={dispatcher}
|
||||
@ -941,10 +1068,48 @@
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample dashed={true}
|
||||
height={394}
|
||||
summary="Desktop room conversation local face-mute"
|
||||
width={298}>
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={482}
|
||||
onContentsRendered={desktopRoomStoreMedium.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation (medium)"
|
||||
width={602}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopRoomConversationView
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mozLoop={navigator.mozLoop}
|
||||
onCallTerminated={function(){}}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
roomStore={desktopRoomStoreMedium} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={485}
|
||||
onContentsRendered={desktopRoomStoreLarge.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation (large)"
|
||||
width={646}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopRoomConversationView
|
||||
dispatcher={dispatcher}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mozLoop={navigator.mozLoop}
|
||||
onCallTerminated={function(){}}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
roomStore={desktopRoomStoreLarge} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample
|
||||
dashed={true}
|
||||
height={394}
|
||||
onContentsRendered={desktopLocalFaceMuteRoomStore.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation local face-mute"
|
||||
width={298}>
|
||||
<div className="fx-embedded">
|
||||
<DesktopRoomConversationView
|
||||
dispatcher={dispatcher}
|
||||
@ -955,7 +1120,9 @@
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<FramedExample dashed={true} height={394}
|
||||
<FramedExample dashed={true}
|
||||
height={394}
|
||||
onContentsRendered={desktopRemoteFaceMuteRoomStore.activeRoomStore.forcedUpdate}
|
||||
summary="Desktop room conversation remote face-mute"
|
||||
width={298} >
|
||||
<div className="fx-embedded">
|
||||
@ -964,6 +1131,7 @@
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
mozLoop={navigator.mozLoop}
|
||||
onCallTerminated={function(){}}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
roomStore={desktopRemoteFaceMuteRoomStore} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
@ -1081,8 +1249,7 @@
|
||||
isFirefox={true}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
screenSharePosterUrl="sample-img/video-screen-baz.png" />
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
@ -1102,8 +1269,7 @@
|
||||
isFirefox={true}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
screenSharePosterUrl="sample-img/video-screen-baz.png" />
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
@ -1171,12 +1337,12 @@
|
||||
cssClass="standalone"
|
||||
dashed={true}
|
||||
height={480}
|
||||
onContentsRendered={updatingActiveRoomStore.forcedUpdate}
|
||||
onContentsRendered={updatingMobileActiveRoomStore.forcedUpdate}
|
||||
summary="Standalone room conversation (has-participants, 600x480)"
|
||||
width={600}>
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
activeRoomStore={updatingActiveRoomStore}
|
||||
activeRoomStore={updatingMobileActiveRoomStore}
|
||||
dispatcher={dispatcher}
|
||||
isFirefox={true}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
@ -1189,12 +1355,12 @@
|
||||
cssClass="standalone"
|
||||
dashed={true}
|
||||
height={480}
|
||||
onContentsRendered={updatingSharingRoomStore.forcedUpdate}
|
||||
onContentsRendered={updatingSharingRoomMobileStore.forcedUpdate}
|
||||
summary="Standalone room convo (has-participants, receivingScreenShare, 600x480)"
|
||||
width={600} >
|
||||
<div className="standalone" cssClass="standalone">
|
||||
<StandaloneRoomView
|
||||
activeRoomStore={updatingSharingRoomStore}
|
||||
activeRoomStore={updatingSharingRoomMobileStore}
|
||||
dispatcher={dispatcher}
|
||||
isFirefox={true}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
@ -1282,7 +1448,7 @@
|
||||
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
var expectedWarningsCount = 23;
|
||||
var expectedWarningsCount = 18;
|
||||
var warningsMismatch = caughtWarnings.length !== expectedWarningsCount;
|
||||
if (uncaughtError || warningsMismatch) {
|
||||
$("#results").append("<div class='failures'><em>" +
|
||||
|
@ -4,7 +4,8 @@
|
||||
* 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/. */
|
||||
|
||||
/* globals overlays, StyleInspectorMenu */
|
||||
/* globals overlays, StyleInspectorMenu, loader, clipboardHelper,
|
||||
_Iterator, StopIteration */
|
||||
|
||||
"use strict";
|
||||
|
||||
@ -49,8 +50,7 @@ const HTML_NS = "http://www.w3.org/1999/xhtml";
|
||||
*
|
||||
* @constructor
|
||||
*/
|
||||
function UpdateProcess(aWin, aGenerator, aOptions)
|
||||
{
|
||||
function UpdateProcess(aWin, aGenerator, aOptions) {
|
||||
this.win = aWin;
|
||||
this.iter = _Iterator(aGenerator);
|
||||
this.onItem = aOptions.onItem || function() {};
|
||||
@ -66,8 +66,7 @@ UpdateProcess.prototype = {
|
||||
/**
|
||||
* Schedule a new batch on the main loop.
|
||||
*/
|
||||
schedule: function UP_schedule()
|
||||
{
|
||||
schedule: function() {
|
||||
if (this.canceled) {
|
||||
return;
|
||||
}
|
||||
@ -78,8 +77,7 @@ UpdateProcess.prototype = {
|
||||
* Cancel the running process. onItem will not be called again,
|
||||
* and onCancel will be called.
|
||||
*/
|
||||
cancel: function UP_cancel()
|
||||
{
|
||||
cancel: function() {
|
||||
if (this._timeout) {
|
||||
this.win.clearTimeout(this._timeout);
|
||||
this._timeout = 0;
|
||||
@ -88,7 +86,7 @@ UpdateProcess.prototype = {
|
||||
this.onCancel();
|
||||
},
|
||||
|
||||
_timeoutHandler: function UP_timeoutHandler() {
|
||||
_timeoutHandler: function() {
|
||||
this._timeout = null;
|
||||
try {
|
||||
this._runBatch();
|
||||
@ -104,10 +102,9 @@ UpdateProcess.prototype = {
|
||||
}
|
||||
},
|
||||
|
||||
_runBatch: function Y_runBatch()
|
||||
{
|
||||
_runBatch: function() {
|
||||
let time = Date.now();
|
||||
while(!this.canceled) {
|
||||
while (!this.canceled) {
|
||||
// Continue until iter.next() throws...
|
||||
let next = this.iter.next();
|
||||
this.onItem(next[1]);
|
||||
@ -120,9 +117,9 @@ UpdateProcess.prototype = {
|
||||
};
|
||||
|
||||
/**
|
||||
* CssComputedView is a panel that manages the display of a table sorted by style.
|
||||
* There should be one instance of CssComputedView per style display (of which there
|
||||
* will generally only be one).
|
||||
* CssComputedView is a panel that manages the display of a table
|
||||
* sorted by style. There should be one instance of CssComputedView
|
||||
* per style display (of which there will generally only be one).
|
||||
*
|
||||
* @param {Inspector} inspector toolbox panel
|
||||
* @param {Document} document The document that will contain the computed view.
|
||||
@ -142,8 +139,8 @@ function CssComputedView(inspector, document, pageStyle) {
|
||||
|
||||
this._outputParser = new OutputParser();
|
||||
|
||||
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"].
|
||||
getService(Ci.nsIXULChromeRegistry);
|
||||
let chromeReg = Cc["@mozilla.org/chrome/chrome-registry;1"]
|
||||
.getService(Ci.nsIXULChromeRegistry);
|
||||
this.getRTLAttr = chromeReg.isLocaleRTL("global") ? "rtl" : "ltr";
|
||||
|
||||
// Create bound methods.
|
||||
@ -211,8 +208,7 @@ function CssComputedView(inspector, document, pageStyle) {
|
||||
* @param {string} aName The key to lookup.
|
||||
* @returns A localized version of the given key.
|
||||
*/
|
||||
CssComputedView.l10n = function CssComputedView_l10n(aName)
|
||||
{
|
||||
CssComputedView.l10n = function(aName) {
|
||||
try {
|
||||
return CssComputedView._strings.GetStringFromName(aName);
|
||||
} catch (ex) {
|
||||
@ -251,8 +247,7 @@ CssComputedView.prototype = {
|
||||
this.pageStyle = pageStyle;
|
||||
},
|
||||
|
||||
get includeBrowserStyles()
|
||||
{
|
||||
get includeBrowserStyles() {
|
||||
return this.includeBrowserStylesCheckbox.checked;
|
||||
},
|
||||
|
||||
@ -264,8 +259,9 @@ CssComputedView.prototype = {
|
||||
},
|
||||
|
||||
/**
|
||||
* Update the view with a new selected element.
|
||||
* The CssComputedView panel will show the style information for the given element.
|
||||
* Update the view with a new selected element. The CssComputedView panel
|
||||
* will show the style information for the given element.
|
||||
*
|
||||
* @param {NodeFront} aElement The highlighted node to get styles for.
|
||||
* @returns a promise that will be resolved when highlighting is complete.
|
||||
*/
|
||||
@ -383,8 +379,7 @@ CssComputedView.prototype = {
|
||||
return {type, value};
|
||||
},
|
||||
|
||||
_createPropertyViews: function()
|
||||
{
|
||||
_createPropertyViews: function() {
|
||||
if (this._createViewsPromise) {
|
||||
return this._createViewsPromise;
|
||||
}
|
||||
@ -396,7 +391,8 @@ CssComputedView.prototype = {
|
||||
this.numVisibleProperties = 0;
|
||||
let fragment = this.styleDocument.createDocumentFragment();
|
||||
|
||||
this._createViewsProcess = new UpdateProcess(this.styleWindow, CssComputedView.propertyNames, {
|
||||
this._createViewsProcess = new UpdateProcess(
|
||||
this.styleWindow, CssComputedView.propertyNames, {
|
||||
onItem: (aPropertyName) => {
|
||||
// Per-item callback.
|
||||
let propView = new PropertyView(this, aPropertyName);
|
||||
@ -426,8 +422,7 @@ CssComputedView.prototype = {
|
||||
/**
|
||||
* Refresh the panel content.
|
||||
*/
|
||||
refreshPanel: function CssComputedView_refreshPanel()
|
||||
{
|
||||
refreshPanel: function() {
|
||||
if (!this.viewedElement) {
|
||||
return promise.resolve();
|
||||
}
|
||||
@ -443,12 +438,12 @@ CssComputedView.prototype = {
|
||||
onlyMatched: !this.includeBrowserStyles,
|
||||
markMatched: true
|
||||
})
|
||||
]).then(([createViews, computed]) => {
|
||||
]).then(([, computed]) => {
|
||||
if (viewedElement !== this.viewedElement) {
|
||||
return;
|
||||
return promise.resolve();
|
||||
}
|
||||
|
||||
this._matchedProperties = new Set;
|
||||
this._matchedProperties = new Set();
|
||||
for (let name in computed) {
|
||||
if (computed[name].matched) {
|
||||
this._matchedProperties.add(name);
|
||||
@ -469,7 +464,8 @@ CssComputedView.prototype = {
|
||||
this._darkStripe = true;
|
||||
|
||||
let deferred = promise.defer();
|
||||
this._refreshProcess = new UpdateProcess(this.styleWindow, this.propertyViews, {
|
||||
this._refreshProcess = new UpdateProcess(
|
||||
this.styleWindow, this.propertyViews, {
|
||||
onItem: (aPropView) => {
|
||||
aPropView.refresh();
|
||||
},
|
||||
@ -511,8 +507,7 @@ CssComputedView.prototype = {
|
||||
*
|
||||
* @param {Event} aEvent the DOM Event object.
|
||||
*/
|
||||
_onFilterStyles: function(aEvent)
|
||||
{
|
||||
_onFilterStyles: function(aEvent) {
|
||||
let win = this.styleWindow;
|
||||
|
||||
if (this._filterChangedTimeout) {
|
||||
@ -580,8 +575,7 @@ CssComputedView.prototype = {
|
||||
*
|
||||
* @param {Event} aEvent the DOM Event object.
|
||||
*/
|
||||
_onIncludeBrowserStyles: function(aEvent)
|
||||
{
|
||||
_onIncludeBrowserStyles: function(aEvent) {
|
||||
this.refreshSourceFilter();
|
||||
this.refreshPanel();
|
||||
},
|
||||
@ -592,16 +586,14 @@ CssComputedView.prototype = {
|
||||
* document or one of thedocument's stylesheets. If .checked is false we
|
||||
* display all properties including those that come from UA stylesheets.
|
||||
*/
|
||||
refreshSourceFilter: function CssComputedView_setSourceFilter()
|
||||
{
|
||||
refreshSourceFilter: function() {
|
||||
this._matchedProperties = null;
|
||||
this._sourceFilter = this.includeBrowserStyles ?
|
||||
CssLogic.FILTER.UA :
|
||||
CssLogic.FILTER.USER;
|
||||
},
|
||||
|
||||
_onSourcePrefChanged: function CssComputedView__onSourcePrefChanged()
|
||||
{
|
||||
_onSourcePrefChanged: function() {
|
||||
for (let propView of this.propertyViews) {
|
||||
propView.updateSourceLinks();
|
||||
}
|
||||
@ -611,8 +603,7 @@ CssComputedView.prototype = {
|
||||
/**
|
||||
* The CSS as displayed by the UI.
|
||||
*/
|
||||
createStyleViews: function CssComputedView_createStyleViews()
|
||||
{
|
||||
createStyleViews: function() {
|
||||
if (CssComputedView.propertyNames) {
|
||||
return;
|
||||
}
|
||||
@ -641,8 +632,8 @@ CssComputedView.prototype = {
|
||||
|
||||
this._createPropertyViews().then(null, e => {
|
||||
if (!this._isDestroyed) {
|
||||
console.warn("The creation of property views was cancelled because the " +
|
||||
"computed-view was destroyed before it was done creating views");
|
||||
console.warn("The creation of property views was cancelled because " +
|
||||
"the computed-view was destroyed before it was done creating views");
|
||||
} else {
|
||||
console.error(e);
|
||||
}
|
||||
@ -654,18 +645,16 @@ CssComputedView.prototype = {
|
||||
*
|
||||
* @return {Set} If a property name is in the set, it has matching selectors.
|
||||
*/
|
||||
get matchedProperties()
|
||||
{
|
||||
return this._matchedProperties || new Set;
|
||||
get matchedProperties() {
|
||||
return this._matchedProperties || new Set();
|
||||
},
|
||||
|
||||
/**
|
||||
* Focus the window on mousedown.
|
||||
*
|
||||
* @param aEvent The event object
|
||||
* @param event The event object
|
||||
*/
|
||||
focusWindow: function(aEvent)
|
||||
{
|
||||
focusWindow: function(event) {
|
||||
let win = this.styleDocument.defaultView;
|
||||
win.focus();
|
||||
},
|
||||
@ -707,8 +696,8 @@ CssComputedView.prototype = {
|
||||
let win = this.styleDocument.defaultView;
|
||||
let text = win.getSelection().toString().trim();
|
||||
|
||||
// Tidy up block headings by moving CSS property names and their values onto
|
||||
// the same line and inserting a colon between them.
|
||||
// Tidy up block headings by moving CSS property names and their
|
||||
// values onto the same line and inserting a colon between them.
|
||||
let textArray = text.split(/[\r\n]+/);
|
||||
let result = "";
|
||||
|
||||
@ -737,8 +726,7 @@ CssComputedView.prototype = {
|
||||
/**
|
||||
* Destructor for CssComputedView.
|
||||
*/
|
||||
destroy: function CssComputedView_destroy()
|
||||
{
|
||||
destroy: function() {
|
||||
this.viewedElement = null;
|
||||
this._outputParser = null;
|
||||
|
||||
@ -819,8 +807,7 @@ PropertyInfo.prototype = {
|
||||
* @param {string} aName the CSS property name for which this PropertyView
|
||||
* instance will render the rules.
|
||||
*/
|
||||
function PropertyView(aTree, aName)
|
||||
{
|
||||
function PropertyView(aTree, aName) {
|
||||
this.tree = aTree;
|
||||
this.name = aName;
|
||||
this.getRTLAttr = aTree.getRTLAttr;
|
||||
@ -864,32 +851,28 @@ PropertyView.prototype = {
|
||||
* @return {string} the computed style for the current property of the
|
||||
* currently highlighted element.
|
||||
*/
|
||||
get value()
|
||||
{
|
||||
get value() {
|
||||
return this.propertyInfo.value;
|
||||
},
|
||||
|
||||
/**
|
||||
* An easy way to access the CssPropertyInfo behind this PropertyView.
|
||||
*/
|
||||
get propertyInfo()
|
||||
{
|
||||
get propertyInfo() {
|
||||
return this._propertyInfo;
|
||||
},
|
||||
|
||||
/**
|
||||
* Does the property have any matched selectors?
|
||||
*/
|
||||
get hasMatchedSelectors()
|
||||
{
|
||||
get hasMatchedSelectors() {
|
||||
return this.tree.matchedProperties.has(this.name);
|
||||
},
|
||||
|
||||
/**
|
||||
* Should this property be visible?
|
||||
*/
|
||||
get visible()
|
||||
{
|
||||
get visible() {
|
||||
if (!this.tree.viewedElement) {
|
||||
return false;
|
||||
}
|
||||
@ -913,8 +896,7 @@ PropertyView.prototype = {
|
||||
* Returns the className that should be assigned to the propertyView.
|
||||
* @return string
|
||||
*/
|
||||
get propertyHeaderClassName()
|
||||
{
|
||||
get propertyHeaderClassName() {
|
||||
if (this.visible) {
|
||||
let isDark = this.tree._darkStripe = !this.tree._darkStripe;
|
||||
return isDark ? "property-view row-striped" : "property-view";
|
||||
@ -927,8 +909,7 @@ PropertyView.prototype = {
|
||||
* container.
|
||||
* @return string
|
||||
*/
|
||||
get propertyContentClassName()
|
||||
{
|
||||
get propertyContentClassName() {
|
||||
if (this.visible) {
|
||||
let isDark = this.tree._darkStripe;
|
||||
return isDark ? "property-content row-striped" : "property-content";
|
||||
@ -940,8 +921,7 @@ PropertyView.prototype = {
|
||||
* Build the markup for on computed style
|
||||
* @return Element
|
||||
*/
|
||||
buildMain: function PropertyView_buildMain()
|
||||
{
|
||||
buildMain: function() {
|
||||
let doc = this.tree.styleDocument;
|
||||
|
||||
// Build the container element
|
||||
@ -998,8 +978,7 @@ PropertyView.prototype = {
|
||||
return this.element;
|
||||
},
|
||||
|
||||
buildSelectorContainer: function PropertyView_buildSelectorContainer()
|
||||
{
|
||||
buildSelectorContainer: function() {
|
||||
let doc = this.tree.styleDocument;
|
||||
let element = doc.createElementNS(HTML_NS, "div");
|
||||
element.setAttribute("class", this.propertyContentClassName);
|
||||
@ -1013,8 +992,7 @@ PropertyView.prototype = {
|
||||
/**
|
||||
* Refresh the panel's CSS property value.
|
||||
*/
|
||||
refresh: function PropertyView_refresh()
|
||||
{
|
||||
refresh: function() {
|
||||
this.element.className = this.propertyHeaderClassName;
|
||||
this.element.nextElementSibling.className = this.propertyContentClassName;
|
||||
|
||||
@ -1051,8 +1029,7 @@ PropertyView.prototype = {
|
||||
/**
|
||||
* Refresh the panel matched rules.
|
||||
*/
|
||||
refreshMatchedSelectors: function PropertyView_refreshMatchedSelectors()
|
||||
{
|
||||
refreshMatchedSelectors: function() {
|
||||
let hasMatchedSelectors = this.hasMatchedSelectors;
|
||||
this.matchedSelectorsContainer.parentNode.hidden = !hasMatchedSelectors;
|
||||
|
||||
@ -1063,28 +1040,29 @@ PropertyView.prototype = {
|
||||
}
|
||||
|
||||
if (this.matchedExpanded && hasMatchedSelectors) {
|
||||
return this.tree.pageStyle.getMatchedSelectors(this.tree.viewedElement, this.name).then(matched => {
|
||||
if (!this.matchedExpanded) {
|
||||
return;
|
||||
}
|
||||
return this.tree.pageStyle
|
||||
.getMatchedSelectors(this.tree.viewedElement, this.name)
|
||||
.then(matched => {
|
||||
if (!this.matchedExpanded) {
|
||||
return promise.resolve(undefined);
|
||||
}
|
||||
|
||||
this._matchedSelectorResponse = matched;
|
||||
this._matchedSelectorResponse = matched;
|
||||
|
||||
return this._buildMatchedSelectors().then(() => {
|
||||
this.matchedExpander.setAttribute("open", "");
|
||||
this.tree.inspector.emit("computed-view-property-expanded");
|
||||
});
|
||||
}).then(null, console.error);
|
||||
} else {
|
||||
this.matchedSelectorsContainer.innerHTML = "";
|
||||
this.matchedExpander.removeAttribute("open");
|
||||
this.tree.inspector.emit("computed-view-property-collapsed");
|
||||
return promise.resolve(undefined);
|
||||
return this._buildMatchedSelectors().then(() => {
|
||||
this.matchedExpander.setAttribute("open", "");
|
||||
this.tree.inspector.emit("computed-view-property-expanded");
|
||||
});
|
||||
}).then(null, console.error);
|
||||
}
|
||||
|
||||
this.matchedSelectorsContainer.innerHTML = "";
|
||||
this.matchedExpander.removeAttribute("open");
|
||||
this.tree.inspector.emit("computed-view-property-collapsed");
|
||||
return promise.resolve(undefined);
|
||||
},
|
||||
|
||||
get matchedSelectors()
|
||||
{
|
||||
get matchedSelectors() {
|
||||
return this._matchedSelectorResponse;
|
||||
},
|
||||
|
||||
@ -1130,13 +1108,13 @@ PropertyView.prototype = {
|
||||
* Provide access to the matched SelectorViews that we are currently
|
||||
* displaying.
|
||||
*/
|
||||
get matchedSelectorViews()
|
||||
{
|
||||
get matchedSelectorViews() {
|
||||
if (!this._matchedSelectorViews) {
|
||||
this._matchedSelectorViews = [];
|
||||
this._matchedSelectorResponse.forEach(
|
||||
function matchedSelectorViews_convert(aSelectorInfo) {
|
||||
this._matchedSelectorViews.push(new SelectorView(this.tree, aSelectorInfo));
|
||||
function(aSelectorInfo) {
|
||||
let selectorView = new SelectorView(this.tree, aSelectorInfo);
|
||||
this._matchedSelectorViews.push(selectorView);
|
||||
}, this);
|
||||
}
|
||||
return this._matchedSelectorViews;
|
||||
@ -1146,8 +1124,7 @@ PropertyView.prototype = {
|
||||
* Update all the selector source links to reflect whether we're linking to
|
||||
* original sources (e.g. Sass files).
|
||||
*/
|
||||
updateSourceLinks: function PropertyView_updateSourceLinks()
|
||||
{
|
||||
updateSourceLinks: function() {
|
||||
if (!this._matchedSelectorViews) {
|
||||
return;
|
||||
}
|
||||
@ -1162,8 +1139,7 @@ PropertyView.prototype = {
|
||||
* @param {Event} aEvent Used to determine the class name of the targets click
|
||||
* event.
|
||||
*/
|
||||
onMatchedToggle: function PropertyView_onMatchedToggle(aEvent)
|
||||
{
|
||||
onMatchedToggle: function(aEvent) {
|
||||
if (aEvent.shiftKey) {
|
||||
return;
|
||||
}
|
||||
@ -1175,8 +1151,7 @@ PropertyView.prototype = {
|
||||
/**
|
||||
* The action when a user clicks on the MDN help link for a property.
|
||||
*/
|
||||
mdnLinkClick: function PropertyView_mdnLinkClick(aEvent)
|
||||
{
|
||||
mdnLinkClick: function(aEvent) {
|
||||
let inspector = this.tree.inspector;
|
||||
|
||||
if (inspector.target.tab) {
|
||||
@ -1189,7 +1164,7 @@ PropertyView.prototype = {
|
||||
/**
|
||||
* Destroy this property view, removing event listeners
|
||||
*/
|
||||
destroy: function PropertyView_destroy() {
|
||||
destroy: function() {
|
||||
this.element.removeEventListener("dblclick", this.onMatchedToggle, false);
|
||||
this.element.removeEventListener("keydown", this.onKeyDown, false);
|
||||
this.element = null;
|
||||
@ -1210,8 +1185,7 @@ PropertyView.prototype = {
|
||||
* @param CssComputedView aTree, the owning CssComputedView
|
||||
* @param aSelectorInfo
|
||||
*/
|
||||
function SelectorView(aTree, aSelectorInfo)
|
||||
{
|
||||
function SelectorView(aTree, aSelectorInfo) {
|
||||
this.tree = aTree;
|
||||
this.selectorInfo = aSelectorInfo;
|
||||
this._cacheStatusNames();
|
||||
@ -1245,8 +1219,7 @@ SelectorView.prototype = {
|
||||
*
|
||||
* @return {void}
|
||||
*/
|
||||
_cacheStatusNames: function SelectorView_cacheStatusNames()
|
||||
{
|
||||
_cacheStatusNames: function() {
|
||||
if (SelectorView.STATUS_NAMES.length) {
|
||||
return;
|
||||
}
|
||||
@ -1256,7 +1229,7 @@ SelectorView.prototype = {
|
||||
if (i > CssLogic.STATUS.UNMATCHED) {
|
||||
let value = CssComputedView.l10n("rule.status." + status);
|
||||
// Replace normal spaces with non-breaking spaces
|
||||
SelectorView.STATUS_NAMES[i] = value.replace(/ /g, '\u00A0');
|
||||
SelectorView.STATUS_NAMES[i] = value.replace(/ /g, "\u00A0");
|
||||
}
|
||||
}
|
||||
},
|
||||
@ -1264,21 +1237,18 @@ SelectorView.prototype = {
|
||||
/**
|
||||
* A localized version of cssRule.status
|
||||
*/
|
||||
get statusText()
|
||||
{
|
||||
get statusText() {
|
||||
return SelectorView.STATUS_NAMES[this.selectorInfo.status];
|
||||
},
|
||||
|
||||
/**
|
||||
* Get class name for selector depending on status
|
||||
*/
|
||||
get statusClass()
|
||||
{
|
||||
get statusClass() {
|
||||
return SelectorView.CLASS_NAMES[this.selectorInfo.status - 1];
|
||||
},
|
||||
|
||||
get href()
|
||||
{
|
||||
get href() {
|
||||
if (this._href) {
|
||||
return this._href;
|
||||
}
|
||||
@ -1287,19 +1257,15 @@ SelectorView.prototype = {
|
||||
return this._href;
|
||||
},
|
||||
|
||||
get sourceText()
|
||||
{
|
||||
get sourceText() {
|
||||
return this.selectorInfo.sourceText;
|
||||
},
|
||||
|
||||
|
||||
get value()
|
||||
{
|
||||
get value() {
|
||||
return this.selectorInfo.value;
|
||||
},
|
||||
|
||||
get outputFragment()
|
||||
{
|
||||
get outputFragment() {
|
||||
// Sadly, because this fragment is added to the template by DOM Templater
|
||||
// we lose any events that are attached. This means that URLs will open in a
|
||||
// new window. At some point we should fix this by stopping using the
|
||||
@ -1320,8 +1286,7 @@ SelectorView.prototype = {
|
||||
* Update the text of the source link to reflect whether we're showing
|
||||
* original sources or not.
|
||||
*/
|
||||
updateSourceLink: function()
|
||||
{
|
||||
updateSourceLink: function() {
|
||||
return this.updateSource().then((oldSource) => {
|
||||
if (oldSource != this.source && this.tree.element) {
|
||||
let selector = '[sourcelocation="' + oldSource + '"]';
|
||||
@ -1337,8 +1302,7 @@ SelectorView.prototype = {
|
||||
/**
|
||||
* Update the 'source' store based on our original sources preference.
|
||||
*/
|
||||
updateSource: function()
|
||||
{
|
||||
updateSource: function() {
|
||||
let rule = this.selectorInfo.rule;
|
||||
this.sheet = rule.parentStyleSheet;
|
||||
|
||||
@ -1373,8 +1337,7 @@ SelectorView.prototype = {
|
||||
/**
|
||||
* Open the style editor if the RETURN key was pressed.
|
||||
*/
|
||||
maybeOpenStyleEditor: function(aEvent)
|
||||
{
|
||||
maybeOpenStyleEditor: function(aEvent) {
|
||||
let keyEvent = Ci.nsIDOMKeyEvent;
|
||||
if (aEvent.keyCode == keyEvent.DOM_VK_RETURN) {
|
||||
this.openStyleEditor();
|
||||
@ -1391,8 +1354,7 @@ SelectorView.prototype = {
|
||||
*
|
||||
* @param aEvent The click event
|
||||
*/
|
||||
openStyleEditor: function(aEvent)
|
||||
{
|
||||
openStyleEditor: function(aEvent) {
|
||||
let inspector = this.tree.inspector;
|
||||
let rule = this.selectorInfo.rule;
|
||||
|
||||
@ -1401,15 +1363,8 @@ SelectorView.prototype = {
|
||||
//
|
||||
// If the stylesheet is a content stylesheet we send it to the style
|
||||
// editor else we display it in the view source window.
|
||||
let sheet = rule.parentStyleSheet;
|
||||
if (!sheet || sheet.isSystem) {
|
||||
let contentDoc = null;
|
||||
if (this.tree.viewedElement.isLocal_toBeDeprecated()) {
|
||||
let rawNode = this.tree.viewedElement.rawNode();
|
||||
if (rawNode) {
|
||||
contentDoc = rawNode.ownerDocument;
|
||||
}
|
||||
}
|
||||
let parentStyleSheet = rule.parentStyleSheet;
|
||||
if (!parentStyleSheet || parentStyleSheet.isSystem) {
|
||||
let toolbox = gDevTools.getToolbox(inspector.target);
|
||||
toolbox.viewSource(rule.href, rule.line);
|
||||
return;
|
||||
@ -1447,7 +1402,7 @@ function createChild(aParent, aTag, aAttributes={}) {
|
||||
if (aAttributes.hasOwnProperty(attr)) {
|
||||
if (attr === "textContent") {
|
||||
elt.textContent = aAttributes[attr];
|
||||
} else if(attr === "child") {
|
||||
} else if (attr === "child") {
|
||||
elt.appendChild(aAttributes[attr]);
|
||||
} else {
|
||||
elt.setAttribute(attr, aAttributes[attr]);
|
||||
|
@ -5,7 +5,8 @@
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/* globals overlays, Services, EventEmitter, StyleInspectorMenu,
|
||||
clipboardHelper, _strings, domUtils, AutocompletePopup */
|
||||
clipboardHelper, _strings, domUtils, AutocompletePopup, loader,
|
||||
osString */
|
||||
|
||||
"use strict";
|
||||
|
||||
@ -209,7 +210,7 @@ ElementStyle.prototype = {
|
||||
filter: this.showUserAgentStyles ? "ua" : undefined,
|
||||
}).then(entries => {
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
return promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Make sure the dummy element has been created before continuing...
|
||||
@ -236,14 +237,12 @@ ElementStyle.prototype = {
|
||||
|
||||
// We're done with the previous list of rules.
|
||||
delete this._refreshRules;
|
||||
|
||||
return null;
|
||||
});
|
||||
}).then(null, e => {
|
||||
// populate is often called after a setTimeout,
|
||||
// the connection may already be closed.
|
||||
if (this.destroyed) {
|
||||
return;
|
||||
return promise.resolve(undefined);
|
||||
}
|
||||
return promiseWarn(e);
|
||||
});
|
||||
@ -636,7 +635,7 @@ Rule.prototype = {
|
||||
disabled.delete(this.style);
|
||||
}
|
||||
|
||||
let promise = aModifications.apply().then(() => {
|
||||
let modificationsPromise = aModifications.apply().then(() => {
|
||||
let cssProps = {};
|
||||
for (let cssProp of parseDeclarations(this.style.cssText)) {
|
||||
cssProps[cssProp.name] = cssProp;
|
||||
@ -668,8 +667,8 @@ Rule.prototype = {
|
||||
this.elementStyle._changed();
|
||||
}).then(null, promiseWarn);
|
||||
|
||||
this._applyingModifications = promise;
|
||||
return promise;
|
||||
this._applyingModifications = modificationsPromise;
|
||||
return modificationsPromise;
|
||||
},
|
||||
|
||||
/**
|
||||
@ -1111,7 +1110,8 @@ TextProperty.prototype = {
|
||||
*/
|
||||
stringifyProperty: function() {
|
||||
// Get the displayed property value
|
||||
let declaration = this.name + ": " + this.editor.committed.value + ";";
|
||||
let declaration = this.name + ": " + this.editor.valueSpan.textContent +
|
||||
";";
|
||||
|
||||
// Comment out property declarations that are not enabled
|
||||
if (!this.enabled) {
|
||||
@ -1741,7 +1741,7 @@ CssRuleView.prototype = {
|
||||
refreshPanel: function() {
|
||||
// Ignore refreshes during editing or when no element is selected.
|
||||
if (this.isEditing || !this._elementStyle) {
|
||||
return;
|
||||
return promise.resolve(undefined);
|
||||
}
|
||||
|
||||
// Repopulate the element style once the current modifications are done.
|
||||
@ -1893,9 +1893,10 @@ CssRuleView.prototype = {
|
||||
|
||||
/**
|
||||
* Creates an expandable container in the rule view
|
||||
* @param {String} aLabel The label for the container header
|
||||
* @param {Boolean} isPseudo Whether or not the container will hold
|
||||
* pseudo element rules
|
||||
* @param {String} aLabel
|
||||
* The label for the container header
|
||||
* @param {Boolean} isPseudo
|
||||
* Whether or not the container will hold pseudo element rules
|
||||
* @return {DOMNode} The container element
|
||||
*/
|
||||
createExpandableContainer: function(aLabel, isPseudo = false) {
|
||||
@ -1915,44 +1916,59 @@ CssRuleView.prototype = {
|
||||
container.classList.add("ruleview-expandable-container");
|
||||
this.element.appendChild(container);
|
||||
|
||||
let toggleContainerVisibility = (isPseudo, showPseudo) => {
|
||||
let isOpen = twisty.getAttribute("open");
|
||||
|
||||
if (isPseudo) {
|
||||
this._showPseudoElements = !!showPseudo;
|
||||
|
||||
Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
|
||||
this.showPseudoElements);
|
||||
|
||||
header.classList.toggle("show-expandable-container",
|
||||
this.showPseudoElements);
|
||||
|
||||
isOpen = !this.showPseudoElements;
|
||||
} else {
|
||||
header.classList.toggle("show-expandable-container");
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
twisty.removeAttribute("open");
|
||||
} else {
|
||||
twisty.setAttribute("open", "true");
|
||||
}
|
||||
};
|
||||
|
||||
header.addEventListener("dblclick", () => {
|
||||
toggleContainerVisibility(isPseudo, !this.showPseudoElements);
|
||||
this._toggleContainerVisibility(twisty, header, isPseudo,
|
||||
!this.showPseudoElements);
|
||||
}, false);
|
||||
|
||||
twisty.addEventListener("click", () => {
|
||||
toggleContainerVisibility(isPseudo, !this.showPseudoElements);
|
||||
this._toggleContainerVisibility(twisty, header, isPseudo,
|
||||
!this.showPseudoElements);
|
||||
}, false);
|
||||
|
||||
if (isPseudo) {
|
||||
toggleContainerVisibility(isPseudo, this.showPseudoElements);
|
||||
this._toggleContainerVisibility(twisty, header, isPseudo,
|
||||
this.showPseudoElements);
|
||||
}
|
||||
|
||||
return container;
|
||||
},
|
||||
|
||||
/**
|
||||
* Toggle the visibility of an expandable container
|
||||
* @param {DOMNode} twisty
|
||||
* clickable toggle DOM Node
|
||||
* @param {DOMNode} header
|
||||
* expandable container header DOM Node
|
||||
* @param {Boolean} isPseudo
|
||||
* whether or not the container will hold pseudo element rules
|
||||
* @param {Boolean} showPseudo
|
||||
* whether or not pseudo element rules should be displayed
|
||||
*/
|
||||
_toggleContainerVisibility: function(twisty, header, isPseudo, showPseudo) {
|
||||
let isOpen = twisty.getAttribute("open");
|
||||
|
||||
if (isPseudo) {
|
||||
this._showPseudoElements = !!showPseudo;
|
||||
|
||||
Services.prefs.setBoolPref("devtools.inspector.show_pseudo_elements",
|
||||
this.showPseudoElements);
|
||||
|
||||
header.classList.toggle("show-expandable-container",
|
||||
this.showPseudoElements);
|
||||
|
||||
isOpen = !this.showPseudoElements;
|
||||
} else {
|
||||
header.classList.toggle("show-expandable-container");
|
||||
}
|
||||
|
||||
if (isOpen) {
|
||||
twisty.removeAttribute("open");
|
||||
} else {
|
||||
twisty.setAttribute("open", "true");
|
||||
}
|
||||
},
|
||||
|
||||
_getRuleViewHeaderClassName: function(isPseudo) {
|
||||
let baseClassName = "theme-gutter ruleview-header";
|
||||
return isPseudo ? baseClassName + " ruleview-expandable-header" :
|
||||
|
@ -231,6 +231,7 @@ StyleInspectorMenu.prototype = {
|
||||
this.menuitemCopy.hidden = !this._hasTextSelected();
|
||||
this.menuitemCopyColor.hidden = !this._isColorPopup();
|
||||
this.menuitemCopyImageDataUrl.hidden = !this._isImageUrl();
|
||||
this.menuitemCopyUrl.hidden = !this._isImageUrl();
|
||||
|
||||
this.menuitemCopyRule.hidden = true;
|
||||
this.menuitemCopyLocation.hidden = true;
|
||||
@ -378,6 +379,10 @@ StyleInspectorMenu.prototype = {
|
||||
* Retrieve the url for the selected image and copy it to the clipboard
|
||||
*/
|
||||
_onCopyUrl: function() {
|
||||
if (!this._clickedNodeInfo) {
|
||||
return;
|
||||
}
|
||||
|
||||
clipboardHelper.copyString(this._clickedNodeInfo.value.url);
|
||||
},
|
||||
|
||||
|
@ -52,6 +52,20 @@ add_task(function*() {
|
||||
copyRule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: "Test Copy Property Value with Priority",
|
||||
node: ruleEditor.rule.textProps[3].editor.valueSpan,
|
||||
menuItem: contextmenu.menuitemCopyPropertyValue,
|
||||
expectedPattern: "#00F !important",
|
||||
hidden: {
|
||||
copyLocation: true,
|
||||
copyPropertyDeclaration: false,
|
||||
copyPropertyName: true,
|
||||
copyPropertyValue: false,
|
||||
copySelector: true,
|
||||
copyRule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: "Test Copy Property Declaration",
|
||||
node: ruleEditor.rule.textProps[2].editor.nameSpan,
|
||||
@ -66,6 +80,20 @@ add_task(function*() {
|
||||
copyRule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: "Test Copy Property Declaration with Priority",
|
||||
node: ruleEditor.rule.textProps[3].editor.nameSpan,
|
||||
menuItem: contextmenu.menuitemCopyPropertyDeclaration,
|
||||
expectedPattern: "border-color: #00F !important;",
|
||||
hidden: {
|
||||
copyLocation: true,
|
||||
copyPropertyDeclaration: false,
|
||||
copyPropertyName: false,
|
||||
copyPropertyValue: true,
|
||||
copySelector: true,
|
||||
copyRule: false
|
||||
}
|
||||
},
|
||||
{
|
||||
desc: "Test Copy Rule",
|
||||
node: ruleEditor.rule.textProps[2].editor.nameSpan,
|
||||
@ -74,6 +102,7 @@ add_task(function*() {
|
||||
"\tcolor: #F00;[\\r\\n]+" +
|
||||
"\tbackground-color: #00F;[\\r\\n]+" +
|
||||
"\tfont-size: 12px;[\\r\\n]+" +
|
||||
"\tborder-color: #00F !important;[\\r\\n]+" +
|
||||
"}",
|
||||
hidden: {
|
||||
copyLocation: true,
|
||||
@ -124,6 +153,7 @@ add_task(function*() {
|
||||
"\t\/\\* color: #F00; \\*\/[\\r\\n]+" +
|
||||
"\tbackground-color: #00F;[\\r\\n]+" +
|
||||
"\tfont-size: 12px;[\\r\\n]+" +
|
||||
"\tborder-color: #00F !important;[\\r\\n]+" +
|
||||
"}",
|
||||
hidden: {
|
||||
copyLocation: true,
|
||||
|
@ -80,6 +80,7 @@ function* testCopyUrlToClipboard({view, inspector}, type, selector, expected) {
|
||||
yield popup;
|
||||
|
||||
info("Context menu is displayed");
|
||||
ok(!view._contextmenu.menuitemCopyUrl.hidden, "\"Copy URL\" menu entry is displayed");
|
||||
ok(!view._contextmenu.menuitemCopyImageDataUrl.hidden, "\"Copy Image Data-URL\" menu entry is displayed");
|
||||
|
||||
if (type == "data-uri") {
|
||||
|
@ -6,4 +6,5 @@ html, body, #testid {
|
||||
color: #F00;
|
||||
background-color: #00F;
|
||||
font-size: 12px;
|
||||
border-color: #00F !important;
|
||||
}
|
||||
|
@ -638,6 +638,7 @@
|
||||
@RESPATH@/components/nsUrlClassifierHashCompleter.js
|
||||
@RESPATH@/components/nsUrlClassifierListManager.js
|
||||
@RESPATH@/components/nsUrlClassifierLib.js
|
||||
@RESPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
|
||||
@RESPATH@/components/url-classifier.xpt
|
||||
#endif
|
||||
|
||||
|
@ -97,6 +97,9 @@ quit-button.tooltiptext.mac = Quit %1$S (%2$S)
|
||||
# approval before you change it.
|
||||
loop-call-button3.label = Hello
|
||||
loop-call-button3.tooltiptext = Start a conversation
|
||||
# LOCALIZATION NOTE(loop-call-button3-pb.tooltiptext): Shown when the button is
|
||||
# placed inside a Private Browsing window. %S is the value of loop-call-button3.label.
|
||||
loop-call-button3-pb.tooltiptext = %S is not available in Private Browsing
|
||||
|
||||
social-share-button.label = Share This Page
|
||||
social-share-button.tooltiptext = Share this page
|
||||
|
@ -206,6 +206,8 @@ active_screenshare_button_title=Stop sharing
|
||||
inactive_screenshare_button_title=Share your screen
|
||||
share_tabs_button_title2=Share your Tabs
|
||||
share_windows_button_title=Share other Windows
|
||||
self_view_hidden_message=Self-view hidden but still being sent; resize window to show
|
||||
|
||||
|
||||
## LOCALIZATION NOTE (call_with_contact_title): The title displayed
|
||||
## when calling a contact. Don't translate the part between {{..}} because
|
||||
|
@ -115,7 +115,7 @@
|
||||
.titlebar-button {
|
||||
border: none;
|
||||
margin: 0 !important;
|
||||
padding: 12px 17px;
|
||||
padding: 10px 17px;
|
||||
}
|
||||
|
||||
#main-window[sizemode=maximized] .titlebar-button {
|
||||
@ -147,6 +147,20 @@
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-white);
|
||||
}
|
||||
|
||||
#titlebar-min:-moz-lwtheme {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-themes);
|
||||
}
|
||||
#titlebar-max:-moz-lwtheme {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-themes);
|
||||
}
|
||||
#main-window[sizemode="maximized"] #titlebar-max:-moz-lwtheme {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-themes);
|
||||
}
|
||||
#titlebar-close:-moz-lwtheme {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-themes);
|
||||
}
|
||||
|
||||
|
||||
/* the 12px image renders a 10px icon, and the 10px upscaled gets rounded to 12.5, which
|
||||
* rounds up to 13px, which makes the icon one pixel too big on 1.25dppx. Fix: */
|
||||
@media (min-resolution: 1.20dppx) and (max-resolution: 1.45dppx) {
|
||||
@ -222,20 +236,32 @@
|
||||
background-color: Highlight;
|
||||
}
|
||||
|
||||
#titlebar-min {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast);
|
||||
}
|
||||
#titlebar-min:hover {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highlight);
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#minimize-highcontrast-hover);
|
||||
}
|
||||
|
||||
#titlebar-max {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast);
|
||||
}
|
||||
#titlebar-max:hover {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highlight);
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#maximize-highcontrast-hover);
|
||||
}
|
||||
|
||||
#main-window[sizemode="maximized"] #titlebar-max {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast);
|
||||
}
|
||||
#main-window[sizemode="maximized"] #titlebar-max:hover {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highlight);
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#restore-highcontrast-hover);
|
||||
}
|
||||
|
||||
#titlebar-close {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast);
|
||||
}
|
||||
#titlebar-close:hover {
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highlight);
|
||||
list-style-image: url(chrome://browser/skin/caption-buttons.svg#close-highcontrast-hover);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -659,7 +659,7 @@ toolbar[brighttext] .toolbarbutton-1 > .toolbarbutton-menubutton-dropmarker {
|
||||
-moz-padding-end: 5px;
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button),
|
||||
#nav-bar .toolbarbutton-1[type=panel],
|
||||
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) {
|
||||
padding-left: 5px;
|
||||
padding-right: 5px;
|
||||
@ -771,8 +771,8 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
||||
width: 32px;
|
||||
}
|
||||
|
||||
#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1[type=panel]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container,
|
||||
#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1[type=panel] > .toolbarbutton-badge-container,
|
||||
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-icon,
|
||||
#nav-bar .toolbarbutton-1[type=menu]:not(#back-button):not(#forward-button):not(#feed-button):not(#PanelUI-menu-button) > .toolbarbutton-badge-container,
|
||||
#nav-bar .toolbarbutton-1[type=menu] > .toolbarbutton-text /* hack for add-ons that forcefully display the label */ {
|
||||
@ -1203,6 +1203,11 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
||||
-moz-padding-end: 2px;
|
||||
}
|
||||
|
||||
/* overlap the urlbar's border */
|
||||
#PopupAutoCompleteRichResult {
|
||||
margin-top: -1px;
|
||||
}
|
||||
|
||||
@media (-moz-os-version: windows-xp),
|
||||
(-moz-os-version: windows-vista),
|
||||
(-moz-os-version: windows-win7) {
|
||||
@ -1246,6 +1251,11 @@ toolbarbutton[constrain-size="true"][cui-areatype="toolbar"] > .toolbarbutton-ba
|
||||
.searchbar-textbox:not(:-moz-lwtheme)[focused] {
|
||||
box-shadow: 0 0 0 1px Highlight inset;
|
||||
}
|
||||
|
||||
/* overlap the urlbar's border and inset box-shadow */
|
||||
#PopupAutoCompleteRichResult:not(:-moz-lwtheme) {
|
||||
margin-top: -2px;
|
||||
}
|
||||
}
|
||||
|
||||
@media not all and (-moz-os-version: windows-xp) {
|
||||
|
@ -9,7 +9,7 @@
|
||||
fill: none;
|
||||
}
|
||||
|
||||
g:not(#close) {
|
||||
g:not([id|="close"]) {
|
||||
shape-rendering: crispEdges;
|
||||
}
|
||||
|
||||
@ -21,17 +21,36 @@
|
||||
display: initial;
|
||||
}
|
||||
|
||||
[id$="-highlight"] > g {
|
||||
g.highlight {
|
||||
stroke-width: 1.9px;
|
||||
}
|
||||
|
||||
g.themes {
|
||||
stroke: #fff;
|
||||
stroke-width: 1.9px;
|
||||
}
|
||||
|
||||
.outer-stroke {
|
||||
stroke: #000;
|
||||
stroke-width: 3.6;
|
||||
opacity: .75;
|
||||
}
|
||||
|
||||
.restore-background-window {
|
||||
stroke-width: .9;
|
||||
}
|
||||
|
||||
[id$="-highcontrast-hover"] > g {
|
||||
stroke: HighlightText;
|
||||
}
|
||||
|
||||
[id$="-white"] > g {
|
||||
stroke: #fff;
|
||||
}
|
||||
|
||||
</style>
|
||||
<g id="close">
|
||||
<line x1="1" y1="1" x2="11" y2="11"/>
|
||||
<line x1="11" y1="1" x2="1" y2="11"/>
|
||||
<path d="M1,1 l 10,10 M1,11 l 10,-10"/>
|
||||
</g>
|
||||
<g id="maximize">
|
||||
<rect x="1.5" y="1.5" width="9" height="9"/>
|
||||
@ -43,13 +62,46 @@
|
||||
<rect x="1.5" y="3.5" width="7" height="7"/>
|
||||
<polyline points="3.5,3.5 3.5,1.5 10.5,1.5 10.5,8.5 8.5,8.5"/>
|
||||
</g>
|
||||
<use id="close-highlight" xlink:href="#close"/>
|
||||
<use id="maximize-highlight" xlink:href="#maximize"/>
|
||||
<use id="minimize-highlight" xlink:href="#minimize"/>
|
||||
<use id="restore-highlight" xlink:href="#restore"/>
|
||||
|
||||
<use id="close-white" xlink:href="#close"/>
|
||||
<use id="maximize-white" xlink:href="#maximize"/>
|
||||
<use id="minimize-white" xlink:href="#minimize"/>
|
||||
<use id="restore-white" xlink:href="#restore"/>
|
||||
|
||||
<g id="close-highcontrast" class="highlight">
|
||||
<path d="M1,1 l 10,10 M1,11 l 10,-10"/>
|
||||
</g>
|
||||
<g id="maximize-highcontrast" class="highlight">
|
||||
<rect x="2" y="2" width="8" height="8"/>
|
||||
</g>
|
||||
<g id="minimize-highcontrast" class="highlight">
|
||||
<line x1="1" y1="6" x2="11" y2="6"/>
|
||||
</g>
|
||||
<g id="restore-highcontrast" class="highlight">
|
||||
<rect x="2" y="4" width="6" height="6"/>
|
||||
<polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/>
|
||||
</g>
|
||||
|
||||
<use id="close-highcontrast-hover" xlink:href="#close-highcontrast"/>
|
||||
<use id="maximize-highcontrast-hover" xlink:href="#maximize-highcontrast"/>
|
||||
<use id="minimize-highcontrast-hover" xlink:href="#minimize-highcontrast"/>
|
||||
<use id="restore-highcontrast-hover" xlink:href="#restore-highcontrast"/>
|
||||
|
||||
<g id="close-themes" class="themes">
|
||||
<path d="M1,1 l 10,10 M1,11 l 10,-10" class="outer-stroke" />
|
||||
<path d="M1.75,1.75 l 8.5,8.5 M1.75,10.25 l 8.5,-8.5"/>
|
||||
</g>
|
||||
<g id="maximize-themes" class="themes">
|
||||
<rect x="2" y="2" width="8" height="8" class="outer-stroke"/>
|
||||
<rect x="2" y="2" width="8" height="8"/>
|
||||
</g>
|
||||
<g id="minimize-themes" class="themes">
|
||||
<line x1="0" y1="6" x2="12" y2="6" class="outer-stroke"/>
|
||||
<line x1="1" y1="6" x2="11" y2="6"/>
|
||||
</g>
|
||||
<g id="restore-themes" class="themes">
|
||||
<path d="M2,4 l 6,0 l 0,6 l -6,0z M2.5,1.5 l 8,0 l 0,8" class="outer-stroke"/>
|
||||
<rect x="2" y="4" width="6" height="6"/>
|
||||
<polyline points="3.5,1.5 10.5,1.5 10.5,8.5" class="restore-background-window"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
Before Width: | Height: | Size: 1.5 KiB After Width: | Height: | Size: 3.0 KiB |
@ -2955,129 +2955,16 @@ nsDocShell::GetRecordProfileTimelineMarkers(bool* aValue)
|
||||
nsresult
|
||||
nsDocShell::PopProfileTimelineMarkers(
|
||||
JSContext* aCx,
|
||||
JS::MutableHandle<JS::Value> aProfileTimelineMarkers)
|
||||
JS::MutableHandle<JS::Value> aOut)
|
||||
{
|
||||
// Looping over all markers gathered so far at the docShell level, whenever a
|
||||
// START marker is found, look for the corresponding END marker and build a
|
||||
// {name,start,end} JS object.
|
||||
// Paint markers are different because paint is handled at root docShell level
|
||||
// in the information that a paint was done is then stored at each sub
|
||||
// docShell level but we can only be sure that a paint did happen in a
|
||||
// docShell if an Layer marker type was recorded too.
|
||||
nsTArray<dom::ProfileTimelineMarker> store;
|
||||
SequenceRooter<dom::ProfileTimelineMarker> rooter(aCx, &store);
|
||||
|
||||
nsTArray<mozilla::dom::ProfileTimelineMarker> profileTimelineMarkers;
|
||||
SequenceRooter<mozilla::dom::ProfileTimelineMarker> rooter(
|
||||
aCx, &profileTimelineMarkers);
|
||||
|
||||
if (!IsObserved()) {
|
||||
if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
|
||||
JS_ClearPendingException(aCx);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
return NS_OK;
|
||||
if (IsObserved()) {
|
||||
mObserved->PopMarkers(aCx, store);
|
||||
}
|
||||
|
||||
nsTArray<UniquePtr<TimelineMarker>>& markersStore = mObserved.get()->mTimelineMarkers;
|
||||
|
||||
// If we see an unpaired START, we keep it around for the next call
|
||||
// to PopProfileTimelineMarkers. We store the kept START objects in
|
||||
// this array.
|
||||
nsTArray<UniquePtr<TimelineMarker>> keptMarkers;
|
||||
|
||||
for (uint32_t i = 0; i < markersStore.Length(); ++i) {
|
||||
UniquePtr<TimelineMarker>& startPayload = markersStore[i];
|
||||
const char* startMarkerName = startPayload->GetName();
|
||||
|
||||
bool hasSeenPaintedLayer = false;
|
||||
bool isPaint = strcmp(startMarkerName, "Paint") == 0;
|
||||
|
||||
// If we are processing a Paint marker, we append information from
|
||||
// all the embedded Layer markers to this array.
|
||||
dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles;
|
||||
|
||||
// If this is a TRACING_TIMESTAMP marker, there's no corresponding "end"
|
||||
// marker, as it's a single unit of time, not a duration, create the final
|
||||
// marker here.
|
||||
if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
|
||||
mozilla::dom::ProfileTimelineMarker* marker =
|
||||
profileTimelineMarkers.AppendElement();
|
||||
|
||||
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
|
||||
marker->mStart = startPayload->GetTime();
|
||||
marker->mEnd = startPayload->GetTime();
|
||||
marker->mStack = startPayload->GetStack();
|
||||
startPayload->AddDetails(aCx, *marker);
|
||||
continue;
|
||||
}
|
||||
|
||||
if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
|
||||
bool hasSeenEnd = false;
|
||||
|
||||
// DOM events can be nested, so we must take care when searching
|
||||
// for the matching end. It doesn't hurt to apply this logic to
|
||||
// all event types.
|
||||
uint32_t markerDepth = 0;
|
||||
|
||||
// The assumption is that the devtools timeline flushes markers frequently
|
||||
// enough for the amount of markers to always be small enough that the
|
||||
// nested for loop isn't going to be a performance problem.
|
||||
for (uint32_t j = i + 1; j < markersStore.Length(); ++j) {
|
||||
UniquePtr<TimelineMarker>& endPayload = markersStore[j];
|
||||
const char* endMarkerName = endPayload->GetName();
|
||||
|
||||
// Look for Layer markers to stream out paint markers.
|
||||
if (isPaint && strcmp(endMarkerName, "Layer") == 0) {
|
||||
hasSeenPaintedLayer = true;
|
||||
endPayload->AddLayerRectangles(layerRectangles);
|
||||
}
|
||||
|
||||
if (!startPayload->Equals(*endPayload)) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pair start and end markers.
|
||||
if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
|
||||
++markerDepth;
|
||||
} else if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
|
||||
if (markerDepth > 0) {
|
||||
--markerDepth;
|
||||
} else {
|
||||
// But ignore paint start/end if no layer has been painted.
|
||||
if (!isPaint || (isPaint && hasSeenPaintedLayer)) {
|
||||
mozilla::dom::ProfileTimelineMarker* marker =
|
||||
profileTimelineMarkers.AppendElement();
|
||||
|
||||
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
|
||||
marker->mStart = startPayload->GetTime();
|
||||
marker->mEnd = endPayload->GetTime();
|
||||
marker->mStack = startPayload->GetStack();
|
||||
if (isPaint) {
|
||||
marker->mRectangles.Construct(layerRectangles);
|
||||
}
|
||||
startPayload->AddDetails(aCx, *marker);
|
||||
endPayload->AddDetails(aCx, *marker);
|
||||
}
|
||||
|
||||
// We want the start to be dropped either way.
|
||||
hasSeenEnd = true;
|
||||
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// If we did not see the corresponding END, keep the START.
|
||||
if (!hasSeenEnd) {
|
||||
keptMarkers.AppendElement(Move(markersStore[i]));
|
||||
markersStore.RemoveElementAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
markersStore.SwapElements(keptMarkers);
|
||||
|
||||
if (!ToJSValue(aCx, profileTimelineMarkers, aProfileTimelineMarkers)) {
|
||||
if (!ToJSValue(aCx, store, aOut)) {
|
||||
JS_ClearPendingException(aCx);
|
||||
return NS_ERROR_UNEXPECTED;
|
||||
}
|
||||
|
@ -34,4 +34,101 @@ ObservedDocShell::ClearMarkers()
|
||||
mTimelineMarkers.Clear();
|
||||
}
|
||||
|
||||
void
|
||||
ObservedDocShell::PopMarkers(JSContext* aCx,
|
||||
nsTArray<dom::ProfileTimelineMarker>& aStore)
|
||||
{
|
||||
// If we see an unpaired START, we keep it around for the next call
|
||||
// to ObservedDocShell::PopMarkers. We store the kept START objects here.
|
||||
nsTArray<UniquePtr<TimelineMarker>> keptStartMarkers;
|
||||
|
||||
for (uint32_t i = 0; i < mTimelineMarkers.Length(); ++i) {
|
||||
UniquePtr<TimelineMarker>& startPayload = mTimelineMarkers[i];
|
||||
|
||||
// If this is a TRACING_TIMESTAMP marker, there's no corresponding END
|
||||
// as it's a single unit of time, not a duration.
|
||||
if (startPayload->GetMetaData() == TRACING_TIMESTAMP) {
|
||||
dom::ProfileTimelineMarker* marker = aStore.AppendElement();
|
||||
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
|
||||
marker->mStart = startPayload->GetTime();
|
||||
marker->mEnd = startPayload->GetTime();
|
||||
marker->mStack = startPayload->GetStack();
|
||||
startPayload->AddDetails(aCx, *marker);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Whenever a START marker is found, look for the corresponding END
|
||||
// and build a {name,start,end} JS object.
|
||||
if (startPayload->GetMetaData() == TRACING_INTERVAL_START) {
|
||||
bool hasSeenEnd = false;
|
||||
|
||||
// "Paint" markers are different because painting is handled at root
|
||||
// docshell level. The information that a paint was done is stored at
|
||||
// sub-docshell level, but we can only be sure that a paint did actually
|
||||
// happen in if a "Layer" marker was recorded too.
|
||||
bool startIsPaintType = strcmp(startPayload->GetName(), "Paint") == 0;
|
||||
bool hasSeenLayerType = false;
|
||||
|
||||
// If we are processing a "Paint" marker, we append information from
|
||||
// all the embedded "Layer" markers to this array.
|
||||
dom::Sequence<dom::ProfileTimelineLayerRect> layerRectangles;
|
||||
|
||||
// DOM events can be nested, so we must take care when searching
|
||||
// for the matching end. It doesn't hurt to apply this logic to
|
||||
// all event types.
|
||||
uint32_t markerDepth = 0;
|
||||
|
||||
// The assumption is that the devtools timeline flushes markers frequently
|
||||
// enough for the amount of markers to always be small enough that the
|
||||
// nested for loop isn't going to be a performance problem.
|
||||
for (uint32_t j = i + 1; j < mTimelineMarkers.Length(); ++j) {
|
||||
UniquePtr<TimelineMarker>& endPayload = mTimelineMarkers[j];
|
||||
bool endIsLayerType = strcmp(endPayload->GetName(), "Layer") == 0;
|
||||
|
||||
// Look for "Layer" markers to stream out "Paint" markers.
|
||||
if (startIsPaintType && endIsLayerType) {
|
||||
hasSeenLayerType = true;
|
||||
endPayload->AddLayerRectangles(layerRectangles);
|
||||
}
|
||||
if (!startPayload->Equals(*endPayload)) {
|
||||
continue;
|
||||
}
|
||||
if (endPayload->GetMetaData() == TRACING_INTERVAL_START) {
|
||||
++markerDepth;
|
||||
continue;
|
||||
}
|
||||
if (endPayload->GetMetaData() == TRACING_INTERVAL_END) {
|
||||
if (markerDepth > 0) {
|
||||
--markerDepth;
|
||||
continue;
|
||||
}
|
||||
if (!startIsPaintType || (startIsPaintType && hasSeenLayerType)) {
|
||||
dom::ProfileTimelineMarker* marker = aStore.AppendElement();
|
||||
marker->mName = NS_ConvertUTF8toUTF16(startPayload->GetName());
|
||||
marker->mStart = startPayload->GetTime();
|
||||
marker->mEnd = endPayload->GetTime();
|
||||
marker->mStack = startPayload->GetStack();
|
||||
if (hasSeenLayerType) {
|
||||
marker->mRectangles.Construct(layerRectangles);
|
||||
}
|
||||
startPayload->AddDetails(aCx, *marker);
|
||||
endPayload->AddDetails(aCx, *marker);
|
||||
}
|
||||
hasSeenEnd = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// If we did not see the corresponding END, keep the START.
|
||||
if (!hasSeenEnd) {
|
||||
keptStartMarkers.AppendElement(Move(mTimelineMarkers[i]));
|
||||
mTimelineMarkers.RemoveElementAt(i);
|
||||
--i;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
mTimelineMarkers.SwapElements(keptStartMarkers);
|
||||
}
|
||||
|
||||
} // namespace mozilla
|
||||
|
@ -15,6 +15,9 @@ class nsDocShell;
|
||||
class TimelineMarker;
|
||||
|
||||
namespace mozilla {
|
||||
namespace dom {
|
||||
struct ProfileTimelineMarker;
|
||||
}
|
||||
|
||||
// # ObservedDocShell
|
||||
//
|
||||
@ -24,18 +27,16 @@ class ObservedDocShell : public LinkedListElement<ObservedDocShell>
|
||||
{
|
||||
private:
|
||||
nsRefPtr<nsDocShell> mDocShell;
|
||||
|
||||
public:
|
||||
// FIXME: make this private once all marker-specific logic has been
|
||||
// moved out of nsDocShell.
|
||||
nsTArray<UniquePtr<TimelineMarker>> mTimelineMarkers;
|
||||
|
||||
public:
|
||||
explicit ObservedDocShell(nsDocShell* aDocShell);
|
||||
nsDocShell* operator*() const { return mDocShell.get(); }
|
||||
|
||||
void AddMarker(const char* aName, TracingMetadata aMetaData);
|
||||
void AddMarker(UniquePtr<TimelineMarker>&& aMarker);
|
||||
void ClearMarkers();
|
||||
void PopMarkers(JSContext* aCx, nsTArray<dom::ProfileTimelineMarker>& aStore);
|
||||
};
|
||||
|
||||
} // namespace mozilla
|
||||
|
@ -898,7 +898,7 @@ MediaPipelineFactory::EnsureExternalCodec(VideoSessionConduit& aConduit,
|
||||
nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
|
||||
if (gfxInfo) {
|
||||
int32_t status;
|
||||
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) {
|
||||
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_ENCODE, &status))) {
|
||||
if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
|
||||
NS_WARNING("VP8 encoder hardware is not whitelisted: disabling.\n");
|
||||
} else {
|
||||
@ -923,11 +923,10 @@ MediaPipelineFactory::EnsureExternalCodec(VideoSessionConduit& aConduit,
|
||||
nsCOMPtr<nsIGfxInfo> gfxInfo = do_GetService("@mozilla.org/gfx/info;1");
|
||||
if (gfxInfo) {
|
||||
int32_t status;
|
||||
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION, &status))) {
|
||||
if (NS_SUCCEEDED(gfxInfo->GetFeatureStatus(nsIGfxInfo::FEATURE_WEBRTC_HW_ACCELERATION_DECODE, &status))) {
|
||||
if (status != nsIGfxInfo::FEATURE_STATUS_OK) {
|
||||
NS_WARNING("VP8 decoder hardware is not whitelisted: disabling.\n");
|
||||
} else {
|
||||
|
||||
VideoDecoder* decoder;
|
||||
decoder = MediaCodecVideoCodec::CreateDecoder(MediaCodecVideoCodec::CodecType::CODEC_VP8);
|
||||
if (decoder) {
|
||||
|
@ -64,6 +64,8 @@ public class AppConstants {
|
||||
* If our MAX_SDK_VERSION is lower than ICS, we must not be an ICS device.
|
||||
* Otherwise, we need a range check.
|
||||
*/
|
||||
public static final boolean preM = MAX_SDK_VERSION < 23 ||
|
||||
(MIN_SDK_VERSION < 23 && Build.VERSION.SDK_INT < 23 && !Build.VERSION.RELEASE.equals("M"));
|
||||
public static final boolean preLollipop = MAX_SDK_VERSION < 21 || (MIN_SDK_VERSION < 21 && Build.VERSION.SDK_INT < 21);
|
||||
public static final boolean preJBMR2 = MAX_SDK_VERSION < 18 || (MIN_SDK_VERSION < 18 && Build.VERSION.SDK_INT < 18);
|
||||
public static final boolean preJBMR1 = MAX_SDK_VERSION < 17 || (MIN_SDK_VERSION < 17 && Build.VERSION.SDK_INT < 17);
|
||||
|
@ -47,6 +47,7 @@ import org.mozilla.gecko.overlays.ui.ShareDialog;
|
||||
import org.mozilla.gecko.prompts.PromptService;
|
||||
import org.mozilla.gecko.util.EventCallback;
|
||||
import org.mozilla.gecko.util.GeckoRequest;
|
||||
import org.mozilla.gecko.util.HardwareCodecCapabilityUtils;
|
||||
import org.mozilla.gecko.util.HardwareUtils;
|
||||
import org.mozilla.gecko.util.NativeEventListener;
|
||||
import org.mozilla.gecko.util.NativeJSContainer;
|
||||
@ -92,6 +93,7 @@ import android.location.LocationManager;
|
||||
import android.net.ConnectivityManager;
|
||||
import android.net.NetworkInfo;
|
||||
import android.net.Uri;
|
||||
import android.os.Build;
|
||||
import android.os.Bundle;
|
||||
import android.os.Environment;
|
||||
import android.os.Handler;
|
||||
@ -961,6 +963,16 @@ public class GeckoAppShell
|
||||
return getHandlersForIntent(intent);
|
||||
}
|
||||
|
||||
@WrapElementForJNI(stubName = "GetHWEncoderCapability")
|
||||
static boolean getHWEncoderCapability() {
|
||||
return HardwareCodecCapabilityUtils.getHWEncoderCapability();
|
||||
}
|
||||
|
||||
@WrapElementForJNI(stubName = "GetHWDecoderCapability")
|
||||
static boolean getHWDecoderCapability() {
|
||||
return HardwareCodecCapabilityUtils.getHWDecoderCapability();
|
||||
}
|
||||
|
||||
static List<ResolveInfo> queryIntentActivities(Intent intent) {
|
||||
final PackageManager pm = getContext().getPackageManager();
|
||||
|
||||
|
@ -237,6 +237,8 @@ public class BrowserSearch extends HomeFragment
|
||||
// Fetch engines if we need to.
|
||||
if (mSearchEngines.isEmpty() || !Locale.getDefault().equals(mLastLocale)) {
|
||||
GeckoAppShell.sendEventToGecko(GeckoEvent.createBroadcastEvent("SearchEngines:GetVisible", null));
|
||||
} else {
|
||||
updateSearchEngineBar();
|
||||
}
|
||||
|
||||
Telemetry.startUISession(TelemetryContract.Session.FRECENCY);
|
||||
@ -342,10 +344,6 @@ public class BrowserSearch extends HomeFragment
|
||||
EventDispatcher.getInstance().registerGeckoThreadListener(this,
|
||||
"SearchEngines:Data");
|
||||
|
||||
// If the view backed by this Fragment is being recreated, we will not receive
|
||||
// a new search engine data event so refresh the new search engine bar's data
|
||||
// & Views with the data we have.
|
||||
updateSearchEngineBar();
|
||||
mSearchEngineBar.setOnSearchBarClickListener(this);
|
||||
}
|
||||
|
||||
|
@ -83,6 +83,7 @@ gujar.sources += [
|
||||
'util/GeckoEventListener.java',
|
||||
'util/GeckoJarReader.java',
|
||||
'util/GeckoRequest.java',
|
||||
'util/HardwareCodecCapabilityUtils.java',
|
||||
'util/HardwareUtils.java',
|
||||
'util/INIParser.java',
|
||||
'util/INISection.java',
|
||||
|
@ -5,6 +5,7 @@
|
||||
|
||||
package org.mozilla.gecko.preferences;
|
||||
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
import org.mozilla.gecko.R;
|
||||
import org.mozilla.gecko.util.ThreadUtils;
|
||||
import org.mozilla.gecko.RestrictedProfiles;
|
||||
@ -26,7 +27,8 @@ class AndroidImportPreference extends MultiPrefMultiChoicePreference {
|
||||
|
||||
public static class Handler implements GeckoPreferences.PrefHandler {
|
||||
public boolean setupPref(Context context, Preference pref) {
|
||||
return RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS);
|
||||
// Feature disabled on devices running Android M+ (Bug 1183559)
|
||||
return Versions.preM && RestrictedProfiles.isAllowed(context, Restriction.DISALLOW_IMPORT_SETTINGS);
|
||||
}
|
||||
|
||||
public void onChange(Context context, Preference pref, Object newValue) { }
|
||||
|
@ -831,7 +831,7 @@ OnSharedPreferenceChangeListener
|
||||
continue;
|
||||
}
|
||||
} else if (PREFS_QRCODE_ENABLED.equals(key)) {
|
||||
if (!AppConstants.NIGHTLY_BUILD || !InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
|
||||
if (!InputOptionsUtils.supportsQrCodeReader(getApplicationContext())) {
|
||||
// Remove UI for qr code input on non nightly builds
|
||||
preferences.removePreference(pref);
|
||||
i--;
|
||||
|
@ -253,10 +253,6 @@ public class ToolbarEditLayout extends ThemedLinearLayout {
|
||||
}
|
||||
|
||||
private boolean qrCodeIsEnabled(Context context) {
|
||||
// QR code is enabled for nightly only
|
||||
if(!AppConstants.NIGHTLY_BUILD) {
|
||||
return false;
|
||||
}
|
||||
final boolean qrCodeIsSupported = InputOptionsUtils.supportsQrCodeReader(context);
|
||||
if (!qrCodeIsSupported) {
|
||||
return false;
|
||||
|
143
mobile/android/base/util/HardwareCodecCapabilityUtils.java
Normal file
143
mobile/android/base/util/HardwareCodecCapabilityUtils.java
Normal file
@ -0,0 +1,143 @@
|
||||
/* -*- Mode: Java; c-basic-offset: 4; tab-width: 20; indent-tabs-mode: nil; -*-
|
||||
* * This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* * License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
|
||||
package org.mozilla.gecko.util;
|
||||
|
||||
import org.mozilla.gecko.AppConstants.Versions;
|
||||
|
||||
import android.media.MediaCodecInfo;
|
||||
import android.media.MediaCodecInfo.CodecCapabilities;
|
||||
import android.media.MediaCodecList;
|
||||
import android.util.Log;
|
||||
|
||||
public final class HardwareCodecCapabilityUtils {
|
||||
private static final String LOGTAG = "GeckoHardwareCodecCapabilityUtils";
|
||||
|
||||
// List of supported HW VP8 encoders.
|
||||
private static final String[] supportedVp8HwEncCodecPrefixes =
|
||||
{"OMX.qcom.", "OMX.Intel." };
|
||||
// List of supported HW VP8 decoders.
|
||||
private static final String[] supportedVp8HwDecCodecPrefixes =
|
||||
{"OMX.qcom.", "OMX.Nvidia.", "OMX.Exynos.", "OMX.Intel." };
|
||||
private static final String VP8_MIME_TYPE = "video/x-vnd.on2.vp8";
|
||||
// NV12 color format supported by QCOM codec, but not declared in MediaCodec -
|
||||
// see /hardware/qcom/media/mm-core/inc/OMX_QCOMExtns.h
|
||||
private static final int
|
||||
COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m = 0x7FA30C04;
|
||||
// Allowable color formats supported by codec - in order of preference.
|
||||
private static final int[] supportedColorList = {
|
||||
CodecCapabilities.COLOR_FormatYUV420Planar,
|
||||
CodecCapabilities.COLOR_FormatYUV420SemiPlanar,
|
||||
CodecCapabilities.COLOR_QCOM_FormatYUV420SemiPlanar,
|
||||
COLOR_QCOM_FORMATYUV420PackedSemiPlanar32m
|
||||
};
|
||||
|
||||
|
||||
public static boolean getHWEncoderCapability() {
|
||||
if (Versions.feature20Plus) {
|
||||
for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
|
||||
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
|
||||
if (!info.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
String name = null;
|
||||
for (String mimeType : info.getSupportedTypes()) {
|
||||
if (mimeType.equals(VP8_MIME_TYPE)) {
|
||||
name = info.getName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name == null) {
|
||||
continue; // No HW support in this codec; try the next one.
|
||||
}
|
||||
Log.e(LOGTAG, "Found candidate encoder " + name);
|
||||
|
||||
// Check if this is supported encoder.
|
||||
boolean supportedCodec = false;
|
||||
for (String codecPrefix : supportedVp8HwEncCodecPrefixes) {
|
||||
if (name.startsWith(codecPrefix)) {
|
||||
supportedCodec = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!supportedCodec) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if codec supports either yuv420 or nv12.
|
||||
CodecCapabilities capabilities =
|
||||
info.getCapabilitiesForType(VP8_MIME_TYPE);
|
||||
for (int colorFormat : capabilities.colorFormats) {
|
||||
Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
|
||||
}
|
||||
for (int supportedColorFormat : supportedColorList) {
|
||||
for (int codecColorFormat : capabilities.colorFormats) {
|
||||
if (codecColorFormat == supportedColorFormat) {
|
||||
// Found supported HW Encoder.
|
||||
Log.e(LOGTAG, "Found target encoder " + name +
|
||||
". Color: 0x" + Integer.toHexString(codecColorFormat));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// No HW encoder.
|
||||
return false;
|
||||
}
|
||||
|
||||
public static boolean getHWDecoderCapability() {
|
||||
if (Versions.feature20Plus) {
|
||||
for (int i = 0; i < MediaCodecList.getCodecCount(); ++i) {
|
||||
MediaCodecInfo info = MediaCodecList.getCodecInfoAt(i);
|
||||
if (info.isEncoder()) {
|
||||
continue;
|
||||
}
|
||||
String name = null;
|
||||
for (String mimeType : info.getSupportedTypes()) {
|
||||
if (mimeType.equals(VP8_MIME_TYPE)) {
|
||||
name = info.getName();
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (name == null) {
|
||||
continue; // No HW support in this codec; try the next one.
|
||||
}
|
||||
Log.e(LOGTAG, "Found candidate decoder " + name);
|
||||
|
||||
// Check if this is supported decoder.
|
||||
boolean supportedCodec = false;
|
||||
for (String codecPrefix : supportedVp8HwDecCodecPrefixes) {
|
||||
if (name.startsWith(codecPrefix)) {
|
||||
supportedCodec = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!supportedCodec) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Check if codec supports either yuv420 or nv12.
|
||||
CodecCapabilities capabilities =
|
||||
info.getCapabilitiesForType(VP8_MIME_TYPE);
|
||||
for (int colorFormat : capabilities.colorFormats) {
|
||||
Log.v(LOGTAG, " Color: 0x" + Integer.toHexString(colorFormat));
|
||||
}
|
||||
for (int supportedColorFormat : supportedColorList) {
|
||||
for (int codecColorFormat : capabilities.colorFormats) {
|
||||
if (codecColorFormat == supportedColorFormat) {
|
||||
// Found supported HW decoder.
|
||||
Log.e(LOGTAG, "Found target decoder " + name +
|
||||
". Color: 0x" + Integer.toHexString(codecColorFormat));
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return false; // No HW decoder.
|
||||
}
|
||||
}
|
@ -38,7 +38,10 @@ public class InputOptionsUtils {
|
||||
|
||||
public static Intent createQRCodeReaderIntent() {
|
||||
// Bug 602818 enables QR code input if you have the particular app below installed in your device
|
||||
Intent intent = new Intent("com.google.zxing.client.android.SCAN");
|
||||
final String appPackage = "com.google.zxing.client.android";
|
||||
|
||||
Intent intent = new Intent(appPackage + ".SCAN");
|
||||
intent.setPackage(appPackage);
|
||||
intent.putExtra("SCAN_MODE", "QR_CODE_MODE");
|
||||
intent.addFlags(Intent.FLAG_ACTIVITY_CLEAR_TOP);
|
||||
return intent;
|
||||
|
@ -218,7 +218,7 @@ let Logins = {
|
||||
if ((newUsername === origUsername) &&
|
||||
(newPassword === origPassword) &&
|
||||
(newDomain === origDomain) ) {
|
||||
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
|
||||
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short");
|
||||
this._showList();
|
||||
return;
|
||||
}
|
||||
@ -240,7 +240,7 @@ let Logins = {
|
||||
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.couldNotSave"), "short");
|
||||
return;
|
||||
}
|
||||
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved"), "short");
|
||||
gChromeWin.NativeWindow.toast.show(gStringBundle.GetStringFromName("editLogin.saved1"), "short");
|
||||
this._showList();
|
||||
},
|
||||
|
||||
|
@ -1759,13 +1759,21 @@ var BrowserApp = {
|
||||
// Add the current host in the 'trackingprotection' consumer of
|
||||
// the permission manager using a normalized URI. This effectively
|
||||
// places this host on the tracking protection white list.
|
||||
Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
|
||||
if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
|
||||
PrivateBrowsingUtils.addToTrackingAllowlist(normalizedUrl);
|
||||
} else {
|
||||
Services.perms.add(normalizedUrl, "trackingprotection", Services.perms.ALLOW_ACTION);
|
||||
}
|
||||
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 1);
|
||||
} else {
|
||||
// Remove the current host from the 'trackingprotection' consumer
|
||||
// of the permission manager. This effectively removes this host
|
||||
// from the tracking protection white list (any list actually).
|
||||
Services.perms.remove(normalizedUrl, "trackingprotection");
|
||||
if (PrivateBrowsingUtils.isBrowserPrivate(browser)) {
|
||||
PrivateBrowsingUtils.removeFromTrackingAllowlist(normalizedUrl);
|
||||
} else {
|
||||
Services.perms.remove(normalizedUrl, "trackingprotection");
|
||||
}
|
||||
Telemetry.addData("TRACKING_PROTECTION_EVENTS", 2);
|
||||
}
|
||||
}
|
||||
|
@ -480,6 +480,7 @@
|
||||
@BINPATH@/components/nsUrlClassifierHashCompleter.js
|
||||
@BINPATH@/components/nsUrlClassifierListManager.js
|
||||
@BINPATH@/components/nsUrlClassifierLib.js
|
||||
@BINPATH@/components/PrivateBrowsingTrackingProtectionWhitelist.js
|
||||
@BINPATH@/components/url-classifier.xpt
|
||||
#endif
|
||||
|
||||
|
@ -14,7 +14,7 @@ loginsDialog.confirm=OK
|
||||
loginsDialog.cancel=Cancel
|
||||
|
||||
editLogin.fallbackTitle=Edit Login
|
||||
editLogin.saved=Saved login
|
||||
editLogin.saved1=Saved login
|
||||
editLogin.couldNotSave=Changes could not be saved
|
||||
|
||||
loginsDetails.age=Age: %S days
|
||||
|
@ -134,6 +134,8 @@ public class StringHelper {
|
||||
public final String SCROLL_TITLE_BAR_LABEL;
|
||||
public final String VOICE_INPUT_TITLE_LABEL;
|
||||
public final String VOICE_INPUT_SUMMARY_LABEL;
|
||||
public final String QRCODE_INPUT_TITLE_LABEL;
|
||||
public final String QRCODE_INPUT_SUMMARY_LABEL;
|
||||
public final String TEXT_REFLOW_LABEL;
|
||||
public final String CHARACTER_ENCODING_LABEL;
|
||||
public final String PLUGINS_LABEL;
|
||||
@ -323,6 +325,8 @@ public class StringHelper {
|
||||
SCROLL_TITLE_BAR_LABEL = res.getString(R.string.pref_scroll_title_bar2);
|
||||
VOICE_INPUT_TITLE_LABEL = res.getString(R.string.pref_voice_input);
|
||||
VOICE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_voice_input_summary);
|
||||
QRCODE_INPUT_TITLE_LABEL = res.getString(R.string.pref_qrcode_enabled);
|
||||
QRCODE_INPUT_SUMMARY_LABEL = res.getString(R.string.pref_qrcode_enabled_summary);
|
||||
TEXT_REFLOW_LABEL = res.getString(R.string.pref_reflow_on_zoom);
|
||||
CHARACTER_ENCODING_LABEL = res.getString(R.string.pref_char_encoding);
|
||||
PLUGINS_LABEL = res.getString(R.string.pref_plugins);
|
||||
|
@ -233,6 +233,12 @@ public class testSettingsMenuItems extends PixelTest {
|
||||
String[] voiceInputUi = { mStringHelper.VOICE_INPUT_TITLE_LABEL, mStringHelper.VOICE_INPUT_SUMMARY_LABEL };
|
||||
settingsMap.get(PATH_DISPLAY).add(voiceInputUi);
|
||||
}
|
||||
|
||||
// QR Code input
|
||||
if (InputOptionsUtils.supportsQrCodeReader(this.getActivity().getApplicationContext())) {
|
||||
String[] qrCodeInputUi = { mStringHelper.QRCODE_INPUT_TITLE_LABEL, mStringHelper.QRCODE_INPUT_SUMMARY_LABEL };
|
||||
settingsMap.get(PATH_DISPLAY).add(qrCodeInputUi);
|
||||
}
|
||||
}
|
||||
|
||||
public void checkMenuHierarchy(Map<String[], List<String[]>> settingsMap) {
|
||||
|
@ -19,6 +19,7 @@
|
||||
#include "nsIIOService.h"
|
||||
#include "nsIParentChannel.h"
|
||||
#include "nsIPermissionManager.h"
|
||||
#include "nsIPrivateBrowsingTrackingProtectionWhitelist.h"
|
||||
#include "nsIProtocolHandler.h"
|
||||
#include "nsIScriptError.h"
|
||||
#include "nsIScriptSecurityManager.h"
|
||||
@ -163,6 +164,25 @@ nsChannelClassifier::ShouldEnableTrackingProtection(nsIChannel *aChannel,
|
||||
*result = true;
|
||||
}
|
||||
|
||||
// In Private Browsing Mode we also check against an in-memory list.
|
||||
if (NS_UsePrivateBrowsing(aChannel)) {
|
||||
nsCOMPtr<nsIPrivateBrowsingTrackingProtectionWhitelist> pbmtpWhitelist =
|
||||
do_GetService(NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID, &rv);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
bool exists = false;
|
||||
rv = pbmtpWhitelist->ExistsInAllowList(topWinURI, &exists);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (exists) {
|
||||
mIsAllowListed = true;
|
||||
LOG(("nsChannelClassifier[%p]: Allowlisting channel[%p] in PBM for %s",
|
||||
this, aChannel, escaped.get()));
|
||||
}
|
||||
|
||||
*result = !exists;
|
||||
}
|
||||
|
||||
// Tracking protection will be enabled so return without updating
|
||||
// the security state. If any channels are subsequently cancelled
|
||||
// (page elements blocked) the state will be then updated.
|
||||
|
@ -72,8 +72,7 @@ let publicProperties = [
|
||||
// }
|
||||
// If the state has changed between the function being called and the promise
|
||||
// being resolved, the .resolve() call will actually be rejected.
|
||||
let AccountState = this.AccountState = function(fxaInternal, storageManager) {
|
||||
this.fxaInternal = fxaInternal;
|
||||
let AccountState = this.AccountState = function(storageManager) {
|
||||
this.storageManager = storageManager;
|
||||
this.promiseInitialized = this.storageManager.getAccountData().then(data => {
|
||||
this.oauthTokens = data && data.oauthTokens ? data.oauthTokens : {};
|
||||
@ -84,13 +83,12 @@ let AccountState = this.AccountState = function(fxaInternal, storageManager) {
|
||||
};
|
||||
|
||||
AccountState.prototype = {
|
||||
cert: null,
|
||||
keyPair: null,
|
||||
oauthTokens: null,
|
||||
whenVerifiedDeferred: null,
|
||||
whenKeysReadyDeferred: null,
|
||||
|
||||
get isCurrent() this.fxaInternal && this.fxaInternal.currentAccountState === this,
|
||||
// If the storage manager has been nuked then we are no longer current.
|
||||
get isCurrent() this.storageManager != null,
|
||||
|
||||
abort() {
|
||||
if (this.whenVerifiedDeferred) {
|
||||
@ -108,7 +106,6 @@ AccountState.prototype = {
|
||||
this.cert = null;
|
||||
this.keyPair = null;
|
||||
this.oauthTokens = null;
|
||||
this.fxaInternal = null;
|
||||
// Avoid finalizing the storageManager multiple times (ie, .signOut()
|
||||
// followed by .abort())
|
||||
if (!this.storageManager) {
|
||||
@ -131,11 +128,14 @@ AccountState.prototype = {
|
||||
});
|
||||
},
|
||||
|
||||
getUserAccountData() {
|
||||
// Get user account data. Optionally specify explcit field names to fetch
|
||||
// (and note that if you require an in-memory field you *must* specify the
|
||||
// field name(s).)
|
||||
getUserAccountData(fieldNames = null) {
|
||||
if (!this.isCurrent) {
|
||||
return Promise.reject(new Error("Another user has signed in"));
|
||||
}
|
||||
return this.storageManager.getAccountData().then(result => {
|
||||
return this.storageManager.getAccountData(fieldNames).then(result => {
|
||||
return this.resolve(result);
|
||||
});
|
||||
},
|
||||
@ -147,66 +147,6 @@ AccountState.prototype = {
|
||||
return this.storageManager.updateAccountData(updatedFields);
|
||||
},
|
||||
|
||||
getCertificate: function(data, keyPair, mustBeValidUntil) {
|
||||
// TODO: get the lifetime from the cert's .exp field
|
||||
if (this.cert && this.cert.validUntil > mustBeValidUntil) {
|
||||
log.debug(" getCertificate already had one");
|
||||
return this.resolve(this.cert.cert);
|
||||
}
|
||||
|
||||
if (Services.io.offline) {
|
||||
return this.reject(new Error(ERROR_OFFLINE));
|
||||
}
|
||||
|
||||
let willBeValidUntil = this.fxaInternal.now() + CERT_LIFETIME;
|
||||
return this.fxaInternal.getCertificateSigned(data.sessionToken,
|
||||
keyPair.serializedPublicKey,
|
||||
CERT_LIFETIME).then(
|
||||
cert => {
|
||||
log.debug("getCertificate got a new one: " + !!cert);
|
||||
this.cert = {
|
||||
cert: cert,
|
||||
validUntil: willBeValidUntil
|
||||
};
|
||||
return cert;
|
||||
}
|
||||
).then(result => this.resolve(result));
|
||||
},
|
||||
|
||||
getKeyPair: function(mustBeValidUntil) {
|
||||
// If the debugging pref to ignore cached authentication credentials is set for Sync,
|
||||
// then don't use any cached key pair, i.e., generate a new one and get it signed.
|
||||
// The purpose of this pref is to expedite any auth errors as the result of a
|
||||
// expired or revoked FxA session token, e.g., from resetting or changing the FxA
|
||||
// password.
|
||||
let ignoreCachedAuthCredentials = false;
|
||||
try {
|
||||
ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
|
||||
} catch(e) {
|
||||
// Pref doesn't exist
|
||||
}
|
||||
if (!ignoreCachedAuthCredentials && this.keyPair && (this.keyPair.validUntil > mustBeValidUntil)) {
|
||||
log.debug("getKeyPair: already have a keyPair");
|
||||
return this.resolve(this.keyPair.keyPair);
|
||||
}
|
||||
// Otherwse, create a keypair and set validity limit.
|
||||
let willBeValidUntil = this.fxaInternal.now() + KEY_LIFETIME;
|
||||
let d = Promise.defer();
|
||||
jwcrypto.generateKeyPair("DS160", (err, kp) => {
|
||||
if (err) {
|
||||
return this.reject(err);
|
||||
}
|
||||
this.keyPair = {
|
||||
keyPair: kp,
|
||||
validUntil: willBeValidUntil
|
||||
};
|
||||
log.debug("got keyPair");
|
||||
delete this.cert;
|
||||
d.resolve(this.keyPair.keyPair);
|
||||
});
|
||||
return d.promise.then(result => this.resolve(result));
|
||||
},
|
||||
|
||||
resolve: function(result) {
|
||||
if (!this.isCurrent) {
|
||||
log.info("An accountState promise was resolved, but was actually rejected" +
|
||||
@ -427,7 +367,7 @@ FxAccountsInternal.prototype = {
|
||||
newAccountState(credentials) {
|
||||
let storage = new FxAccountsStorageManager();
|
||||
storage.initialize(credentials);
|
||||
return new AccountState(this, storage);
|
||||
return new AccountState(storage);
|
||||
},
|
||||
|
||||
/**
|
||||
@ -559,6 +499,52 @@ FxAccountsInternal.prototype = {
|
||||
})
|
||||
},
|
||||
|
||||
/**
|
||||
* returns a promise that fires with the keypair.
|
||||
*/
|
||||
getKeyPair: Task.async(function* (mustBeValidUntil) {
|
||||
// If the debugging pref to ignore cached authentication credentials is set for Sync,
|
||||
// then don't use any cached key pair, i.e., generate a new one and get it signed.
|
||||
// The purpose of this pref is to expedite any auth errors as the result of a
|
||||
// expired or revoked FxA session token, e.g., from resetting or changing the FxA
|
||||
// password.
|
||||
let ignoreCachedAuthCredentials = false;
|
||||
try {
|
||||
ignoreCachedAuthCredentials = Services.prefs.getBoolPref("services.sync.debug.ignoreCachedAuthCredentials");
|
||||
} catch(e) {
|
||||
// Pref doesn't exist
|
||||
}
|
||||
let currentState = this.currentAccountState;
|
||||
let accountData = yield currentState.getUserAccountData("keyPair");
|
||||
if (!ignoreCachedAuthCredentials && accountData.keyPair && (accountData.keyPair.validUntil > mustBeValidUntil)) {
|
||||
log.debug("getKeyPair: already have a keyPair");
|
||||
return accountData.keyPair.keyPair;
|
||||
}
|
||||
// Otherwse, create a keypair and set validity limit.
|
||||
let willBeValidUntil = this.now() + KEY_LIFETIME;
|
||||
let kp = yield new Promise((resolve, reject) => {
|
||||
jwcrypto.generateKeyPair("DS160", (err, kp) => {
|
||||
if (err) {
|
||||
return reject(err);
|
||||
}
|
||||
log.debug("got keyPair");
|
||||
let toUpdate = {
|
||||
keyPair: {
|
||||
keyPair: kp,
|
||||
validUntil: willBeValidUntil
|
||||
},
|
||||
cert: null
|
||||
};
|
||||
currentState.updateUserAccountData(toUpdate).then(() => {
|
||||
resolve(kp);
|
||||
}).catch(err => {
|
||||
log.error("Failed to update account data with keypair and cert");
|
||||
});
|
||||
});
|
||||
});
|
||||
return kp;
|
||||
}),
|
||||
|
||||
/**
|
||||
* returns a promise that fires with the assertion. If there is no verified
|
||||
* signed-in user, fires with null.
|
||||
@ -576,8 +562,8 @@ FxAccountsInternal.prototype = {
|
||||
// Signed-in user has not verified email
|
||||
return null;
|
||||
}
|
||||
return currentState.getKeyPair(mustBeValidUntil).then(keyPair => {
|
||||
return currentState.getCertificate(data, keyPair, mustBeValidUntil)
|
||||
return this.getKeyPair(mustBeValidUntil).then(keyPair => {
|
||||
return this.getCertificate(data, keyPair, mustBeValidUntil)
|
||||
.then(cert => {
|
||||
return this.getAssertionFromCert(data, keyPair, cert, audience);
|
||||
});
|
||||
@ -845,6 +831,37 @@ FxAccountsInternal.prototype = {
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* returns a promise that fires with a certificate.
|
||||
*/
|
||||
getCertificate: Task.async(function* (data, keyPair, mustBeValidUntil) {
|
||||
// TODO: get the lifetime from the cert's .exp field
|
||||
let currentState = this.currentAccountState;
|
||||
let accountData = yield currentState.getUserAccountData("cert");
|
||||
if (accountData.cert && accountData.cert.validUntil > mustBeValidUntil) {
|
||||
log.debug(" getCertificate already had one");
|
||||
return accountData.cert.cert;
|
||||
}
|
||||
if (Services.io.offline) {
|
||||
throw new Error(ERROR_OFFLINE);
|
||||
}
|
||||
let willBeValidUntil = this.now() + CERT_LIFETIME;
|
||||
let cert = yield this.getCertificateSigned(data.sessionToken,
|
||||
keyPair.serializedPublicKey,
|
||||
CERT_LIFETIME);
|
||||
log.debug("getCertificate got a new one: " + !!cert);
|
||||
if (cert) {
|
||||
let toUpdate = {
|
||||
cert: {
|
||||
cert: cert,
|
||||
validUntil: willBeValidUntil
|
||||
}
|
||||
};
|
||||
yield currentState.updateUserAccountData(toUpdate);
|
||||
}
|
||||
return cert;
|
||||
}),
|
||||
|
||||
getUserAccountData: function() {
|
||||
return this.currentAccountState.getUserAccountData();
|
||||
},
|
||||
|
@ -212,13 +212,22 @@ exports.ERROR_MSG_METHOD_NOT_ALLOWED = "METHOD_NOT_ALLOWED";
|
||||
|
||||
// FxAccounts has the ability to "split" the credentials between a plain-text
|
||||
// JSON file in the profile dir and in the login manager.
|
||||
// These constants relate to that.
|
||||
// In order to prevent new fields accidentally ending up in the "wrong" place,
|
||||
// all fields stored are listed here.
|
||||
|
||||
// The fields we save in the plaintext JSON.
|
||||
// See bug 1013064 comments 23-25 for why the sessionToken is "safe"
|
||||
exports.FXA_PWDMGR_PLAINTEXT_FIELDS = ["email", "verified", "authAt",
|
||||
"sessionToken", "uid", "oauthTokens",
|
||||
"profile"];
|
||||
exports.FXA_PWDMGR_PLAINTEXT_FIELDS = new Set(
|
||||
["email", "verified", "authAt", "sessionToken", "uid", "oauthTokens", "profile"]);
|
||||
|
||||
// Fields we store in secure storage if it exists.
|
||||
exports.FXA_PWDMGR_SECURE_FIELDS = new Set(
|
||||
["kA", "kB", "keyFetchToken", "unwrapBKey", "assertion"]);
|
||||
|
||||
// Fields we keep in memory and don't persist anywhere.
|
||||
exports.FXA_PWDMGR_MEMORY_FIELDS = new Set(
|
||||
["cert", "keyPair"]);
|
||||
|
||||
// The pseudo-host we use in the login manager
|
||||
exports.FXA_PWDMGR_HOST = "chrome://FirefoxAccounts";
|
||||
// The realm we use in the login manager.
|
||||
|
@ -62,10 +62,18 @@ this.FxAccountsStorageManager.prototype = {
|
||||
this._needToReadSecure = false;
|
||||
// split it into the 2 parts, write it and we are done.
|
||||
for (let [name, val] of Iterator(accountData)) {
|
||||
if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
|
||||
if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
|
||||
this.cachedPlain[name] = val;
|
||||
} else {
|
||||
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
|
||||
this.cachedSecure[name] = val;
|
||||
} else {
|
||||
// Hopefully it's an "in memory" field. If it's not we log a warning
|
||||
// but still treat it as such (so it will still be available in this
|
||||
// session but isn't persisted anywhere.)
|
||||
if (!FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
|
||||
log.warn("Unknown FxA field name in user data, treating as in-memory", name);
|
||||
}
|
||||
this.cachedMemory[name] = val;
|
||||
}
|
||||
}
|
||||
// write it out and we are done.
|
||||
@ -121,7 +129,12 @@ this.FxAccountsStorageManager.prototype = {
|
||||
},
|
||||
|
||||
// Get the account data by combining the plain and secure storage.
|
||||
getAccountData: Task.async(function* () {
|
||||
// If fieldNames is specified, it may be a string or an array of strings,
|
||||
// and only those fields are returned. If not specified the entire account
|
||||
// data is returned except for "in memory" fields. Note that not specifying
|
||||
// field names will soon be deprecated/removed - we want all callers to
|
||||
// specify the fields they care about.
|
||||
getAccountData: Task.async(function* (fieldNames = null) {
|
||||
yield this._promiseInitialized;
|
||||
// We know we are initialized - this means our .cachedPlain is accurate
|
||||
// and doesn't need to be read (it was read if necessary by initialize).
|
||||
@ -130,21 +143,53 @@ this.FxAccountsStorageManager.prototype = {
|
||||
return null;
|
||||
}
|
||||
let result = {};
|
||||
for (let [name, value] of Iterator(this.cachedPlain)) {
|
||||
result[name] = value;
|
||||
if (fieldNames === null) {
|
||||
// The "old" deprecated way of fetching a logged in user.
|
||||
for (let [name, value] of Iterator(this.cachedPlain)) {
|
||||
result[name] = value;
|
||||
}
|
||||
// But the secure data may not have been read, so try that now.
|
||||
yield this._maybeReadAndUpdateSecure();
|
||||
// .cachedSecure now has as much as it possibly can (which is possibly
|
||||
// nothing if (a) secure storage remains locked and (b) we've never updated
|
||||
// a field to be stored in secure storage.)
|
||||
for (let [name, value] of Iterator(this.cachedSecure)) {
|
||||
result[name] = value;
|
||||
}
|
||||
// Note we don't return cachedMemory fields here - they must be explicitly
|
||||
// requested.
|
||||
return result;
|
||||
}
|
||||
// But the secure data may not have been read, so try that now.
|
||||
yield this._maybeReadAndUpdateSecure();
|
||||
// .cachedSecure now has as much as it possibly can (which is possibly
|
||||
// nothing if (a) secure storage remains locked and (b) we've never updated
|
||||
// a field to be stored in secure storage.)
|
||||
for (let [name, value] of Iterator(this.cachedSecure)) {
|
||||
result[name] = value;
|
||||
// The new explicit way of getting attributes.
|
||||
if (!Array.isArray(fieldNames)) {
|
||||
fieldNames = [fieldNames];
|
||||
}
|
||||
let checkedSecure = false;
|
||||
for (let fieldName of fieldNames) {
|
||||
if (FXA_PWDMGR_MEMORY_FIELDS.has(fieldName)) {
|
||||
if (this.cachedMemory[fieldName] !== undefined) {
|
||||
result[fieldName] = this.cachedMemory[fieldName];
|
||||
}
|
||||
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(fieldName)) {
|
||||
if (this.cachedPlain[fieldName] !== undefined) {
|
||||
result[fieldName] = this.cachedPlain[fieldName];
|
||||
}
|
||||
} else if (FXA_PWDMGR_SECURE_FIELDS.has(fieldName)) {
|
||||
// We may not have read secure storage yet.
|
||||
if (!checkedSecure) {
|
||||
yield this._maybeReadAndUpdateSecure();
|
||||
checkedSecure = true;
|
||||
}
|
||||
if (this.cachedSecure[fieldName] !== undefined) {
|
||||
result[fieldName] = this.cachedSecure[fieldName];
|
||||
}
|
||||
} else {
|
||||
throw new Error("unexpected field '" + name + "'");
|
||||
}
|
||||
}
|
||||
return result;
|
||||
}),
|
||||
|
||||
|
||||
// Update just the specified fields. This DOES NOT allow you to change to
|
||||
// a different user, nor to set the user as signed-out.
|
||||
updateAccountData: Task.async(function* (newFields) {
|
||||
@ -163,16 +208,27 @@ this.FxAccountsStorageManager.prototype = {
|
||||
log.debug("_updateAccountData with items", Object.keys(newFields));
|
||||
// work out what bucket.
|
||||
for (let [name, value] of Iterator(newFields)) {
|
||||
if (FXA_PWDMGR_PLAINTEXT_FIELDS.indexOf(name) >= 0) {
|
||||
if (FXA_PWDMGR_MEMORY_FIELDS.has(name)) {
|
||||
if (value == null) {
|
||||
delete this.cachedMemory[name];
|
||||
} else {
|
||||
this.cachedMemory[name] = value;
|
||||
}
|
||||
} else if (FXA_PWDMGR_PLAINTEXT_FIELDS.has(name)) {
|
||||
if (value == null) {
|
||||
delete this.cachedPlain[name];
|
||||
} else {
|
||||
this.cachedPlain[name] = value;
|
||||
}
|
||||
} else {
|
||||
} else if (FXA_PWDMGR_SECURE_FIELDS.has(name)) {
|
||||
// don't do the "delete on null" thing here - we need to keep it until
|
||||
// we have managed to read so we can nuke it on write.
|
||||
this.cachedSecure[name] = value;
|
||||
} else {
|
||||
// Throwing seems reasonable here as some client code has explicitly
|
||||
// specified the field name, so it's either confused or needs to update
|
||||
// how this field is to be treated.
|
||||
throw new Error("unexpected field '" + name + "'");
|
||||
}
|
||||
}
|
||||
// If we haven't yet read the secure data, do so now, else we may write
|
||||
@ -185,6 +241,7 @@ this.FxAccountsStorageManager.prototype = {
|
||||
}),
|
||||
|
||||
_clearCachedData() {
|
||||
this.cachedMemory = {};
|
||||
this.cachedPlain = {};
|
||||
// If we don't have secure storage available we have cachedPlain and
|
||||
// cachedSecure be the same object.
|
||||
|
@ -155,7 +155,7 @@ function MockFxAccounts() {
|
||||
// we use a real accountState but mocked storage.
|
||||
let storage = new MockStorageManager();
|
||||
storage.initialize(credentials);
|
||||
return new AccountState(this, storage);
|
||||
return new AccountState(storage);
|
||||
},
|
||||
getCertificateSigned: function (sessionToken, serializedPublicKey) {
|
||||
_("mock getCertificateSigned\n");
|
||||
@ -202,7 +202,7 @@ add_task(function test_get_signed_in_user_initially_unset() {
|
||||
// we use a real accountState but mocked storage.
|
||||
let storage = new MockStorageManager();
|
||||
storage.initialize(credentials);
|
||||
return new AccountState(this, storage);
|
||||
return new AccountState(storage);
|
||||
},
|
||||
});
|
||||
let credentials = {
|
||||
@ -251,7 +251,7 @@ add_task(function* test_getCertificate() {
|
||||
// we use a real accountState but mocked storage.
|
||||
let storage = new MockStorageManager();
|
||||
storage.initialize(credentials);
|
||||
return new AccountState(this, storage);
|
||||
return new AccountState(storage);
|
||||
},
|
||||
});
|
||||
let credentials = {
|
||||
@ -272,7 +272,7 @@ add_task(function* test_getCertificate() {
|
||||
let offline = Services.io.offline;
|
||||
Services.io.offline = true;
|
||||
// This call would break from missing parameters ...
|
||||
yield fxa.internal.currentAccountState.getCertificate().then(
|
||||
yield fxa.internal.getCertificate().then(
|
||||
result => {
|
||||
Services.io.offline = offline;
|
||||
do_throw("Unexpected success");
|
||||
@ -505,8 +505,9 @@ add_task(function test_getAssertion() {
|
||||
_("ASSERTION: " + assertion + "\n");
|
||||
let pieces = assertion.split("~");
|
||||
do_check_eq(pieces[0], "cert1");
|
||||
let keyPair = fxa.internal.currentAccountState.keyPair;
|
||||
let cert = fxa.internal.currentAccountState.cert;
|
||||
let userData = yield fxa.getSignedInUser();
|
||||
let keyPair = userData.keyPair;
|
||||
let cert = userData.cert;
|
||||
do_check_neq(keyPair, undefined);
|
||||
_(keyPair.validUntil + "\n");
|
||||
let p2 = pieces[1].split(".");
|
||||
@ -553,9 +554,10 @@ add_task(function test_getAssertion() {
|
||||
// expiration time of the assertion should be different. We compare this to
|
||||
// the initial start time, to which they are relative, not the current value
|
||||
// of "now".
|
||||
userData = yield fxa.getSignedInUser();
|
||||
|
||||
keyPair = fxa.internal.currentAccountState.keyPair;
|
||||
cert = fxa.internal.currentAccountState.cert;
|
||||
keyPair = userData.keyPair;
|
||||
cert = userData.cert;
|
||||
do_check_eq(keyPair.validUntil, start + KEY_LIFETIME);
|
||||
do_check_eq(cert.validUntil, start + CERT_LIFETIME);
|
||||
exp = Number(payload.exp);
|
||||
@ -576,8 +578,9 @@ add_task(function test_getAssertion() {
|
||||
header = JSON.parse(atob(p2[0]));
|
||||
payload = JSON.parse(atob(p2[1]));
|
||||
do_check_eq(payload.aud, "fourth.example.com");
|
||||
keyPair = fxa.internal.currentAccountState.keyPair;
|
||||
cert = fxa.internal.currentAccountState.cert;
|
||||
userData = yield fxa.getSignedInUser();
|
||||
keyPair = userData.keyPair;
|
||||
cert = userData.cert;
|
||||
do_check_eq(keyPair.validUntil, now + KEY_LIFETIME);
|
||||
do_check_eq(cert.validUntil, now + CERT_LIFETIME);
|
||||
exp = Number(payload.exp);
|
||||
|
@ -85,7 +85,7 @@ function MockFxAccounts() {
|
||||
// we use a real accountState but mocked storage.
|
||||
let storage = new MockStorageManager();
|
||||
storage.initialize(credentials);
|
||||
return new AccountState(this, storage);
|
||||
return new AccountState(storage);
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -51,9 +51,11 @@ function MockedSecureStorage(accountData) {
|
||||
}
|
||||
|
||||
MockedSecureStorage.prototype = {
|
||||
fetchCount: 0,
|
||||
locked: false,
|
||||
STORAGE_LOCKED: function() {},
|
||||
get: Task.async(function* (uid, email) {
|
||||
this.fetchCount++;
|
||||
if (this.locked) {
|
||||
throw new this.STORAGE_LOCKED();
|
||||
}
|
||||
@ -85,7 +87,7 @@ add_storage_task(function* checkInitializedEmpty(sm) {
|
||||
}
|
||||
yield sm.initialize();
|
||||
Assert.strictEqual((yield sm.getAccountData()), null);
|
||||
Assert.rejects(sm.updateAccountData({foo: "bar"}), "No user is logged in")
|
||||
Assert.rejects(sm.updateAccountData({kA: "kA"}), "No user is logged in")
|
||||
});
|
||||
|
||||
// Initialized with account data (ie, simulating a new user being logged in).
|
||||
@ -130,9 +132,9 @@ add_storage_task(function* checkEverythingRead(sm) {
|
||||
Assert.equal(accountData.email, "someone@somewhere.com");
|
||||
// Update the data - we should be able to fetch it back and it should appear
|
||||
// in our storage.
|
||||
yield sm.updateAccountData({verified: true, foo: "bar", kA: "kA"});
|
||||
yield sm.updateAccountData({verified: true, kA: "kA", kB: "kB"});
|
||||
accountData = yield sm.getAccountData();
|
||||
Assert.equal(accountData.foo, "bar");
|
||||
Assert.equal(accountData.kB, "kB");
|
||||
Assert.equal(accountData.kA, "kA");
|
||||
// Check the new value was written to storage.
|
||||
yield sm._promiseStorageComplete; // storage is written in the background.
|
||||
@ -141,10 +143,10 @@ add_storage_task(function* checkEverythingRead(sm) {
|
||||
// "kA" and "foo" are secure
|
||||
if (sm.secureStorage) {
|
||||
Assert.equal(sm.secureStorage.data.accountData.kA, "kA");
|
||||
Assert.equal(sm.secureStorage.data.accountData.foo, "bar");
|
||||
Assert.equal(sm.secureStorage.data.accountData.kB, "kB");
|
||||
} else {
|
||||
Assert.equal(sm.plainStorage.data.accountData.kA, "kA");
|
||||
Assert.equal(sm.plainStorage.data.accountData.foo, "bar");
|
||||
Assert.equal(sm.plainStorage.data.accountData.kB, "kB");
|
||||
}
|
||||
});
|
||||
|
||||
@ -231,6 +233,53 @@ add_task(function* checkEverythingReadSecure() {
|
||||
Assert.equal(accountData.kA, "kA");
|
||||
});
|
||||
|
||||
add_task(function* checkMemoryFieldsNotReturnedByDefault() {
|
||||
let sm = new FxAccountsStorageManager();
|
||||
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
|
||||
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
|
||||
yield sm.initialize();
|
||||
|
||||
// keyPair is a memory field.
|
||||
yield sm.updateAccountData({keyPair: "the keypair value"});
|
||||
let accountData = yield sm.getAccountData();
|
||||
|
||||
// Requesting everything should *not* return in memory fields.
|
||||
Assert.strictEqual(accountData.keyPair, undefined);
|
||||
// But requesting them specifically does get them.
|
||||
accountData = yield sm.getAccountData("keyPair");
|
||||
Assert.strictEqual(accountData.keyPair, "the keypair value");
|
||||
});
|
||||
|
||||
add_task(function* checkExplicitGet() {
|
||||
let sm = new FxAccountsStorageManager();
|
||||
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
|
||||
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
|
||||
yield sm.initialize();
|
||||
|
||||
let accountData = yield sm.getAccountData(["uid", "kA"]);
|
||||
Assert.ok(accountData, "read account data");
|
||||
Assert.equal(accountData.uid, "uid");
|
||||
Assert.equal(accountData.kA, "kA");
|
||||
// We didn't ask for email so shouldn't have got it.
|
||||
Assert.strictEqual(accountData.email, undefined);
|
||||
});
|
||||
|
||||
add_task(function* checkExplicitGetNoSecureRead() {
|
||||
let sm = new FxAccountsStorageManager();
|
||||
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
|
||||
sm.secureStorage = new MockedSecureStorage({kA: "kA"});
|
||||
yield sm.initialize();
|
||||
|
||||
Assert.equal(sm.secureStorage.fetchCount, 0);
|
||||
// request 2 fields in secure storage - it should have caused a single fetch.
|
||||
let accountData = yield sm.getAccountData(["email", "uid"]);
|
||||
Assert.ok(accountData, "read account data");
|
||||
Assert.equal(accountData.uid, "uid");
|
||||
Assert.equal(accountData.email, "someone@somewhere.com");
|
||||
Assert.strictEqual(accountData.kA, undefined);
|
||||
Assert.equal(sm.secureStorage.fetchCount, 1);
|
||||
});
|
||||
|
||||
add_task(function* checkLockedUpdates() {
|
||||
let sm = new FxAccountsStorageManager();
|
||||
sm.plainStorage = new MockedPlainStorage({uid: "uid", email: "someone@somewhere.com"})
|
||||
|
@ -181,17 +181,17 @@ this.configureFxAccountIdentity = function(authService,
|
||||
}
|
||||
let storageManager = new MockFxaStorageManager();
|
||||
storageManager.initialize(config.fxaccount.user);
|
||||
let accountState = new AccountState(this, storageManager);
|
||||
// mock getCertificate
|
||||
accountState.getCertificate = function(data, keyPair, mustBeValidUntil) {
|
||||
accountState.cert = {
|
||||
validUntil: fxa.internal.now() + CERT_LIFETIME,
|
||||
cert: "certificate",
|
||||
};
|
||||
return Promise.resolve(this.cert.cert);
|
||||
}
|
||||
let accountState = new AccountState(storageManager);
|
||||
return accountState;
|
||||
}
|
||||
},
|
||||
getCertificate(data, keyPair, mustBeValidUntil) {
|
||||
let cert = {
|
||||
validUntil: this.now() + CERT_LIFETIME,
|
||||
cert: "certificate",
|
||||
};
|
||||
this.currentAccountState.updateUserAccountData({cert: cert});
|
||||
return Promise.resolve(cert.cert);
|
||||
},
|
||||
};
|
||||
fxa = new FxAccounts(MockInternal);
|
||||
|
||||
|
@ -595,7 +595,7 @@ add_task(function test_getKeysMissing() {
|
||||
}
|
||||
let storageManager = new MockFxaStorageManager();
|
||||
storageManager.initialize(identityConfig.fxaccount.user);
|
||||
return new AccountState(this, storageManager);
|
||||
return new AccountState(storageManager);
|
||||
},
|
||||
});
|
||||
|
||||
@ -674,7 +674,7 @@ function* initializeIdentityWithHAWKResponseFactory(config, cbGetResponse) {
|
||||
}
|
||||
let storageManager = new MockFxaStorageManager();
|
||||
storageManager.initialize(config.fxaccount.user);
|
||||
return new AccountState(this, storageManager);
|
||||
return new AccountState(storageManager);
|
||||
},
|
||||
}
|
||||
let fxa = new FxAccounts(internal);
|
||||
|
@ -348,7 +348,7 @@ OptimizeIconSize(IconData& aIcon,
|
||||
|
||||
AsyncFaviconHelperBase::AsyncFaviconHelperBase(
|
||||
nsCOMPtr<nsIFaviconDataCallback>& aCallback
|
||||
) : mDB(Database::GetDatabase())
|
||||
)
|
||||
{
|
||||
// Don't AddRef or Release in runnables for thread-safety.
|
||||
mCallback.swap(aCallback);
|
||||
@ -451,7 +451,9 @@ AsyncFetchAndSetIconForPage::Run()
|
||||
"This should not be called on the main thread");
|
||||
|
||||
// Try to fetch the icon from the database.
|
||||
nsresult rv = FetchIconInfo(mDB, mIcon);
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
NS_ENSURE_STATE(DB);
|
||||
nsresult rv = FetchIconInfo(DB, mIcon);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
bool isInvalidIcon = mIcon.data.IsEmpty() ||
|
||||
@ -464,7 +466,7 @@ AsyncFetchAndSetIconForPage::Run()
|
||||
// directly proceed with association.
|
||||
nsRefPtr<AsyncAssociateIconToPage> event =
|
||||
new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
|
||||
mDB->DispatchToAsyncThread(event);
|
||||
DB->DispatchToAsyncThread(event);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
@ -695,9 +697,11 @@ AsyncFetchAndSetIconFromNetwork::OnStopRequest(nsIRequest* aRequest,
|
||||
|
||||
mIcon.status = ICON_STATUS_CHANGED;
|
||||
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
NS_ENSURE_STATE(DB);
|
||||
nsRefPtr<AsyncAssociateIconToPage> event =
|
||||
new AsyncAssociateIconToPage(mIcon, mPage, mCallback);
|
||||
mDB->DispatchToAsyncThread(event);
|
||||
DB->DispatchToAsyncThread(event);
|
||||
|
||||
return NS_OK;
|
||||
}
|
||||
@ -725,7 +729,9 @@ AsyncAssociateIconToPage::Run()
|
||||
NS_PRECONDITION(!NS_IsMainThread(),
|
||||
"This should not be called on the main thread");
|
||||
|
||||
nsresult rv = FetchPageInfo(mDB, mPage);
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
NS_ENSURE_STATE(DB);
|
||||
nsresult rv = FetchPageInfo(DB, mPage);
|
||||
if (rv == NS_ERROR_NOT_AVAILABLE){
|
||||
// We have never seen this page. If we can add the page to history,
|
||||
// we will try to do it later, otherwise just bail out.
|
||||
@ -737,19 +743,19 @@ AsyncAssociateIconToPage::Run()
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
mozStorageTransaction transaction(mDB->MainConn(), false,
|
||||
mozStorageTransaction transaction(DB->MainConn(), false,
|
||||
mozIStorageConnection::TRANSACTION_IMMEDIATE);
|
||||
|
||||
// If there is no entry for this icon, or the entry is obsolete, replace it.
|
||||
if (mIcon.id == 0 || (mIcon.status & ICON_STATUS_CHANGED)) {
|
||||
rv = SetIconInfo(mDB, mIcon);
|
||||
rv = SetIconInfo(DB, mIcon);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Get the new icon id. Do this regardless mIcon.id, since other code
|
||||
// could have added a entry before us. Indeed we interrupted the thread
|
||||
// after the previous call to FetchIconInfo.
|
||||
mIcon.status = (mIcon.status & ~(ICON_STATUS_CACHED)) | ICON_STATUS_SAVED;
|
||||
rv = FetchIconInfo(mDB, mIcon);
|
||||
rv = FetchIconInfo(DB, mIcon);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
|
||||
@ -764,7 +770,7 @@ AsyncAssociateIconToPage::Run()
|
||||
if (mPage.iconId != mIcon.id) {
|
||||
nsCOMPtr<mozIStorageStatement> stmt;
|
||||
if (mPage.id) {
|
||||
stmt = mDB->GetStatement(
|
||||
stmt = DB->GetStatement(
|
||||
"UPDATE moz_places SET favicon_id = :icon_id WHERE id = :page_id"
|
||||
);
|
||||
NS_ENSURE_STATE(stmt);
|
||||
@ -772,7 +778,7 @@ AsyncAssociateIconToPage::Run()
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
}
|
||||
else {
|
||||
stmt = mDB->GetStatement(
|
||||
stmt = DB->GetStatement(
|
||||
"UPDATE moz_places SET favicon_id = :icon_id WHERE url = :page_url"
|
||||
);
|
||||
NS_ENSURE_STATE(stmt);
|
||||
@ -846,8 +852,10 @@ AsyncGetFaviconURLForPage::Run()
|
||||
NS_PRECONDITION(!NS_IsMainThread(),
|
||||
"This should not be called on the main thread.");
|
||||
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
NS_ENSURE_STATE(DB);
|
||||
nsAutoCString iconSpec;
|
||||
nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec);
|
||||
nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// Now notify our callback of the icon spec we retrieved, even if empty.
|
||||
@ -911,8 +919,10 @@ AsyncGetFaviconDataForPage::Run()
|
||||
NS_PRECONDITION(!NS_IsMainThread(),
|
||||
"This should not be called on the main thread.");
|
||||
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
NS_ENSURE_STATE(DB);
|
||||
nsAutoCString iconSpec;
|
||||
nsresult rv = FetchIconURL(mDB, mPageSpec, iconSpec);
|
||||
nsresult rv = FetchIconURL(DB, mPageSpec, iconSpec);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
IconData iconData;
|
||||
@ -922,7 +932,7 @@ AsyncGetFaviconDataForPage::Run()
|
||||
pageData.spec.Assign(mPageSpec);
|
||||
|
||||
if (!iconSpec.IsEmpty()) {
|
||||
rv = FetchIconInfo(mDB, iconData);
|
||||
rv = FetchIconInfo(DB, iconData);
|
||||
if (NS_FAILED(rv)) {
|
||||
iconData.spec.Truncate();
|
||||
}
|
||||
@ -975,16 +985,18 @@ AsyncReplaceFaviconData::Run()
|
||||
NS_PRECONDITION(!NS_IsMainThread(),
|
||||
"This should not be called on the main thread");
|
||||
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
NS_ENSURE_STATE(DB);
|
||||
IconData dbIcon;
|
||||
dbIcon.spec.Assign(mIcon.spec);
|
||||
nsresult rv = FetchIconInfo(mDB, dbIcon);
|
||||
nsresult rv = FetchIconInfo(DB, dbIcon);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
if (!dbIcon.id) {
|
||||
return NS_OK;
|
||||
}
|
||||
|
||||
rv = SetIconInfo(mDB, mIcon);
|
||||
rv = SetIconInfo(DB, mIcon);
|
||||
NS_ENSURE_SUCCESS(rv, rv);
|
||||
|
||||
// We can invalidate the cache version since we now persist the icon.
|
||||
@ -1096,11 +1108,14 @@ NotifyIconObservers::SendGlobalNotifications(nsIURI* aIconURI)
|
||||
PageData bookmarkedPage;
|
||||
bookmarkedPage.spec = mPage.bookmarkedSpec;
|
||||
|
||||
nsRefPtr<Database> DB = Database::GetDatabase();
|
||||
if (!DB)
|
||||
return;
|
||||
// This will be silent, so be sure to not pass in the current callback.
|
||||
nsCOMPtr<nsIFaviconDataCallback> nullCallback;
|
||||
nsRefPtr<AsyncAssociateIconToPage> event =
|
||||
new AsyncAssociateIconToPage(mIcon, bookmarkedPage, nullCallback);
|
||||
mDB->DispatchToAsyncThread(event);
|
||||
DB->DispatchToAsyncThread(event);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -102,7 +102,6 @@ protected:
|
||||
|
||||
virtual ~AsyncFaviconHelperBase();
|
||||
|
||||
nsRefPtr<Database> mDB;
|
||||
// Strong reference since we are responsible for its existence.
|
||||
nsCOMPtr<nsIFaviconDataCallback> mCallback;
|
||||
};
|
||||
|
@ -315,8 +315,8 @@ public:
|
||||
* `true` if we have not started shutdown, i.e. if
|
||||
* `BlockShutdown()` hasn't been called yet, false otherwise.
|
||||
*/
|
||||
bool IsStarted() const {
|
||||
return mIsStarted;
|
||||
static bool IsStarted() {
|
||||
return sIsStarted;
|
||||
}
|
||||
|
||||
private:
|
||||
@ -354,21 +354,22 @@ private:
|
||||
NOTIFIED_OBSERVERS_PLACES_CONNECTION_CLOSED,
|
||||
};
|
||||
State mState;
|
||||
bool mIsStarted;
|
||||
|
||||
// As tests may resurrect a dead `Database`, we use a counter to
|
||||
// give the instances of `DatabaseShutdown` unique names.
|
||||
uint16_t mCounter;
|
||||
static uint16_t sCounter;
|
||||
|
||||
static Atomic<bool> sIsStarted;
|
||||
|
||||
~DatabaseShutdown() {}
|
||||
};
|
||||
uint16_t DatabaseShutdown::sCounter = 0;
|
||||
Atomic<bool> DatabaseShutdown::sIsStarted(false);
|
||||
|
||||
DatabaseShutdown::DatabaseShutdown(Database* aDatabase)
|
||||
: mDatabase(aDatabase)
|
||||
, mState(NOT_STARTED)
|
||||
, mIsStarted(false)
|
||||
, mCounter(sCounter++)
|
||||
{
|
||||
MOZ_ASSERT(NS_IsMainThread());
|
||||
@ -465,7 +466,7 @@ DatabaseShutdown::BlockShutdown(nsIAsyncShutdownClient* aParentClient)
|
||||
{
|
||||
mParentClient = aParentClient;
|
||||
mState = RECEIVED_BLOCK_SHUTDOWN;
|
||||
mIsStarted = true;
|
||||
sIsStarted = true;
|
||||
|
||||
if (NS_WARN_IF(!mBarrier)) {
|
||||
return NS_ERROR_NOT_AVAILABLE;
|
||||
@ -662,6 +663,15 @@ Database::GetConnectionShutdown()
|
||||
return mConnectionShutdown->GetClient();
|
||||
}
|
||||
|
||||
// static
|
||||
already_AddRefed<Database>
|
||||
Database::GetDatabase()
|
||||
{
|
||||
if (DatabaseShutdown::IsStarted())
|
||||
return nullptr;
|
||||
return GetSingleton();
|
||||
}
|
||||
|
||||
nsresult
|
||||
Database::Init()
|
||||
{
|
||||
|
@ -94,10 +94,7 @@ public:
|
||||
*
|
||||
* @return Singleton instance of this class.
|
||||
*/
|
||||
static already_AddRefed<Database> GetDatabase()
|
||||
{
|
||||
return GetSingleton();
|
||||
}
|
||||
static already_AddRefed<Database> GetDatabase();
|
||||
|
||||
/**
|
||||
* Returns last known database status.
|
||||
|
@ -146,7 +146,6 @@ function gzipCompressString(string) {
|
||||
return observer.buffer;
|
||||
}
|
||||
|
||||
|
||||
this.TelemetrySend = {
|
||||
|
||||
/**
|
||||
@ -391,8 +390,12 @@ let SendScheduler = {
|
||||
let pending = TelemetryStorage.getPendingPingList();
|
||||
let current = TelemetrySendImpl.getUnpersistedPings();
|
||||
this._log.trace("_doSendTask - pending: " + pending.length + ", current: " + current.length);
|
||||
pending = pending.filter(p => TelemetrySendImpl.sendingEnabled(p));
|
||||
current = current.filter(p => TelemetrySendImpl.sendingEnabled(p));
|
||||
// Note that the two lists contain different kind of data. |pending| only holds ping
|
||||
// info, while |current| holds actual ping data.
|
||||
if (!TelemetrySendImpl.sendingEnabled()) {
|
||||
pending = pending.filter(pingInfo => TelemetryStorage.isDeletionPing(pingInfo.id));
|
||||
current = current.filter(p => isDeletionPing(p));
|
||||
}
|
||||
this._log.trace("_doSendTask - can send - pending: " + pending.length + ", current: " + current.length);
|
||||
|
||||
// Bail out if there is nothing to send.
|
||||
@ -712,7 +715,12 @@ let TelemetrySendImpl = {
|
||||
yield this._doPing(ping, ping.id, false);
|
||||
} catch (ex) {
|
||||
this._log.info("sendPings - ping " + ping.id + " not sent, saving to disk", ex);
|
||||
yield TelemetryStorage.savePendingPing(ping);
|
||||
// Deletion pings must be saved to a special location.
|
||||
if (isDeletionPing(ping)) {
|
||||
yield TelemetryStorage.saveDeletionPing(ping);
|
||||
} else {
|
||||
yield TelemetryStorage.savePendingPing(ping);
|
||||
}
|
||||
} finally {
|
||||
this._currentPings.delete(ping.id);
|
||||
}
|
||||
@ -784,6 +792,9 @@ let TelemetrySendImpl = {
|
||||
}
|
||||
|
||||
if (success && isPersisted) {
|
||||
if (TelemetryStorage.isDeletionPing(id)) {
|
||||
return TelemetryStorage.removeDeletionPing();
|
||||
}
|
||||
return TelemetryStorage.removePendingPing(id);
|
||||
} else {
|
||||
return Promise.resolve();
|
||||
|
@ -34,6 +34,7 @@ const Utils = TelemetryUtils;
|
||||
const DATAREPORTING_DIR = "datareporting";
|
||||
const PINGS_ARCHIVE_DIR = "archived";
|
||||
const ABORTED_SESSION_FILE_NAME = "aborted-session-ping";
|
||||
const DELETION_PING_FILE_NAME = "pending-deletion-ping";
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "gDataReportingDir", function() {
|
||||
return OS.Path.join(OS.Constants.Path.profileDir, DATAREPORTING_DIR);
|
||||
@ -44,6 +45,9 @@ XPCOMUtils.defineLazyGetter(this, "gPingsArchivePath", function() {
|
||||
XPCOMUtils.defineLazyGetter(this, "gAbortedSessionFilePath", function() {
|
||||
return OS.Path.join(gDataReportingDir, ABORTED_SESSION_FILE_NAME);
|
||||
});
|
||||
XPCOMUtils.defineLazyGetter(this, "gDeletionPingFilePath", function() {
|
||||
return OS.Path.join(gDataReportingDir, DELETION_PING_FILE_NAME);
|
||||
});
|
||||
|
||||
// Maxmimum time, in milliseconds, archive pings should be retained.
|
||||
const MAX_ARCHIVED_PINGS_RETENTION_MS = 180 * 24 * 60 * 60 * 1000; // 180 days
|
||||
@ -262,6 +266,30 @@ this.TelemetryStorage = {
|
||||
return TelemetryStorageImpl.loadAbortedSessionPing();
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the deletion ping.
|
||||
* @param ping The deletion ping.
|
||||
* @return {Promise} A promise resolved when the ping is saved.
|
||||
*/
|
||||
saveDeletionPing: function(ping) {
|
||||
return TelemetryStorageImpl.saveDeletionPing(ping);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the deletion ping.
|
||||
* @return {Promise} Resolved when the ping is deleted from the disk.
|
||||
*/
|
||||
removeDeletionPing: function() {
|
||||
return TelemetryStorageImpl.removeDeletionPing();
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the ping id identifies a deletion ping.
|
||||
*/
|
||||
isDeletionPing: function(aPingId) {
|
||||
return TelemetryStorageImpl.isDeletionPing(aPingId);
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the aborted-session ping if present.
|
||||
*
|
||||
@ -482,6 +510,8 @@ let TelemetryStorageImpl = {
|
||||
_logger: null,
|
||||
// Used to serialize aborted session ping writes to disk.
|
||||
_abortedSessionSerializer: new SaveSerializer(),
|
||||
// Used to serialize deletion ping writes to disk.
|
||||
_deletionPingSerializer: new SaveSerializer(),
|
||||
|
||||
// Tracks the archived pings in a Map of (id -> {timestampCreated, type}).
|
||||
// We use this to cache info on archived pings to avoid scanning the disk more than once.
|
||||
@ -521,6 +551,7 @@ let TelemetryStorageImpl = {
|
||||
shutdown: Task.async(function*() {
|
||||
this._shutdown = true;
|
||||
yield this._abortedSessionSerializer.flushTasks();
|
||||
yield this._deletionPingSerializer.flushTasks();
|
||||
// If the tasks for archive cleaning or pending ping quota are still running, block on
|
||||
// them. They will bail out as soon as possible.
|
||||
yield this._cleanArchiveTask;
|
||||
@ -1225,6 +1256,18 @@ let TelemetryStorageImpl = {
|
||||
}
|
||||
|
||||
yield iter.close();
|
||||
|
||||
// Explicitly load the deletion ping from its known path, if it's there.
|
||||
if (yield OS.File.exists(gDeletionPingFilePath)) {
|
||||
this._log.trace("_scanPendingPings - Adding pending deletion ping.");
|
||||
// We can't get the ping id or the last modification date without hitting the disk.
|
||||
// Since deletion has a special handling, we don't really need those.
|
||||
this._pendingPings.set(Utils.generateUUID(), {
|
||||
path: gDeletionPingFilePath,
|
||||
lastModificationDate: Date.now(),
|
||||
});
|
||||
}
|
||||
|
||||
this._scannedPendingDirectory = true;
|
||||
return this._buildPingList();
|
||||
}),
|
||||
@ -1379,6 +1422,50 @@ let TelemetryStorageImpl = {
|
||||
}
|
||||
}.bind(this)));
|
||||
},
|
||||
|
||||
/**
|
||||
* Save the deletion ping.
|
||||
* @param ping The deletion ping.
|
||||
* @return {Promise} Resolved when the ping is saved.
|
||||
*/
|
||||
saveDeletionPing: Task.async(function*(ping) {
|
||||
this._log.trace("saveDeletionPing - ping path: " + gDeletionPingFilePath);
|
||||
yield OS.File.makeDir(gDataReportingDir, { ignoreExisting: true });
|
||||
|
||||
return this._deletionPingSerializer.enqueueTask(() =>
|
||||
this.savePingToFile(ping, gDeletionPingFilePath, true));
|
||||
}),
|
||||
|
||||
/**
|
||||
* Remove the deletion ping.
|
||||
* @return {Promise} Resolved when the ping is deleted from the disk.
|
||||
*/
|
||||
removeDeletionPing: Task.async(function*() {
|
||||
return this._deletionPingSerializer.enqueueTask(Task.async(function*() {
|
||||
try {
|
||||
yield OS.File.remove(gDeletionPingFilePath, { ignoreAbsent: false });
|
||||
this._log.trace("removeDeletionPing - success");
|
||||
} catch (ex if ex.becauseNoSuchFile) {
|
||||
this._log.trace("removeDeletionPing - no such file");
|
||||
} catch (ex) {
|
||||
this._log.error("removeDeletionPing - error removing ping", ex)
|
||||
}
|
||||
}.bind(this)));
|
||||
}),
|
||||
|
||||
isDeletionPing: function(aPingId) {
|
||||
this._log.trace("isDeletionPing - id: " + aPingId);
|
||||
let pingInfo = this._pendingPings.get(aPingId);
|
||||
if (!pingInfo) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (pingInfo.path != gDeletionPingFilePath) {
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
};
|
||||
|
||||
///// Utility functions
|
||||
|
@ -152,11 +152,37 @@ add_task(function* test_deletionPing() {
|
||||
return;
|
||||
}
|
||||
|
||||
const PREF_TELEMETRY_SERVER = "toolkit.telemetry.server";
|
||||
|
||||
// Disable FHR upload: this should trigger a deletion ping.
|
||||
Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
|
||||
|
||||
let ping = yield PingServer.promiseNextPing();
|
||||
checkPingFormat(ping, DELETION_PING_TYPE, true, false);
|
||||
// Wait on ping activity to settle.
|
||||
yield TelemetrySend.testWaitOnOutgoingPings();
|
||||
|
||||
// Restore FHR Upload.
|
||||
Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
|
||||
|
||||
// Simulate a failure in sending the deletion ping by disabling the HTTP server.
|
||||
yield PingServer.stop();
|
||||
// Disable FHR upload to send a deletion ping again.
|
||||
Preferences.set(PREF_FHR_UPLOAD_ENABLED, false);
|
||||
// Wait for the send task to terminate, flagging it to do so at the next opportunity and
|
||||
// cancelling any timeouts.
|
||||
yield TelemetryController.reset();
|
||||
|
||||
// Enable the ping server again.
|
||||
PingServer.start();
|
||||
// We set the new server using the pref, otherwise it would get reset with
|
||||
// |TelemetryController.reset|.
|
||||
Preferences.set(PREF_TELEMETRY_SERVER, "http://localhost:" + PingServer.port);
|
||||
|
||||
// Reset the controller to spin the ping sending task.
|
||||
yield TelemetryController.reset();
|
||||
ping = yield PingServer.promiseNextPing();
|
||||
checkPingFormat(ping, DELETION_PING_TYPE, true, false);
|
||||
|
||||
// Restore FHR Upload.
|
||||
Preferences.set(PREF_FHR_UPLOAD_ENABLED, true);
|
||||
@ -262,7 +288,7 @@ add_task(function* test_archivePings() {
|
||||
add_task(function* test_midnightPingSendFuzzing() {
|
||||
const fuzzingDelay = 60 * 60 * 1000;
|
||||
fakeMidnightPingFuzzingDelay(fuzzingDelay);
|
||||
let now = new Date(2030, 5, 1, 11, 00, 0);
|
||||
let now = new Date(2030, 5, 1, 11, 0, 0);
|
||||
fakeNow(now);
|
||||
|
||||
let waitForTimer = () => new Promise(resolve => {
|
||||
|
@ -0,0 +1,68 @@
|
||||
/* 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/. */
|
||||
|
||||
const Ci = Components.interfaces;
|
||||
const Cu = Components.utils;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
function PrivateBrowsingTrackingProtectionWhitelist() {
|
||||
// The list of URIs explicitly excluded from tracking protection.
|
||||
this._allowlist = [];
|
||||
|
||||
Services.obs.addObserver(this, "last-pb-context-exited", true);
|
||||
}
|
||||
|
||||
PrivateBrowsingTrackingProtectionWhitelist.prototype = {
|
||||
classID: Components.ID("{a319b616-c45d-4037-8d86-01c592b5a9af}"),
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIPrivateBrowsingTrackingProtectionWhitelist,
|
||||
Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference,
|
||||
Ci.nsISupports]),
|
||||
_xpcom_factory: XPCOMUtils.generateSingletonFactory(PrivateBrowsingTrackingProtectionWhitelist),
|
||||
|
||||
/**
|
||||
* Add the provided URI to the list of allowed tracking sites.
|
||||
*
|
||||
* @param uri nsIURI
|
||||
* The URI to add to the list.
|
||||
*/
|
||||
addToAllowList(uri) {
|
||||
if (this._allowlist.indexOf(uri.spec) === -1) {
|
||||
this._allowlist.push(uri.spec);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the provided URI from the list of allowed tracking sites.
|
||||
*
|
||||
* @param uri nsIURI
|
||||
* The URI to add to the list.
|
||||
*/
|
||||
removeFromAllowList(uri) {
|
||||
let index = this._allowlist.indexOf(uri.spec);
|
||||
if (index !== -1) {
|
||||
this._allowlist.splice(index, 1);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if the provided URI exists in the list of allowed tracking sites.
|
||||
*
|
||||
* @param uri nsIURI
|
||||
* The URI to add to the list.
|
||||
*/
|
||||
existsInAllowList(uri) {
|
||||
return this._allowlist.indexOf(uri.spec) !== -1;
|
||||
},
|
||||
|
||||
observe: function (subject, topic, data) {
|
||||
if (topic == "last-pb-context-exited") {
|
||||
this._allowlist = [];
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory([PrivateBrowsingTrackingProtectionWhitelist]);
|
@ -7,6 +7,7 @@
|
||||
TEST_DIRS += ['tests']
|
||||
|
||||
XPIDL_SOURCES += [
|
||||
'nsIPrivateBrowsingTrackingProtectionWhitelist.idl',
|
||||
'nsIUrlClassifierDBService.idl',
|
||||
'nsIUrlClassifierHashCompleter.idl',
|
||||
'nsIUrlClassifierPrefixSet.idl',
|
||||
@ -42,6 +43,7 @@ SOURCES += [
|
||||
EXTRA_COMPONENTS += [
|
||||
'nsURLClassifier.manifest',
|
||||
'nsUrlClassifierHashCompleter.js',
|
||||
'PrivateBrowsingTrackingProtectionWhitelist.js',
|
||||
]
|
||||
|
||||
# Same as JS components that are run through the pre-processor.
|
||||
|
@ -0,0 +1,46 @@
|
||||
/* 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/. */
|
||||
|
||||
#include "nsISupports.idl"
|
||||
|
||||
interface nsIURI;
|
||||
|
||||
/**
|
||||
* The Private Browsing Tracking Protection service checks a URI against an
|
||||
* in-memory list of tracking sites.
|
||||
*/
|
||||
[scriptable, uuid(c77ddfac-6cd6-43a9-84e8-91682a1a7b18)]
|
||||
interface nsIPrivateBrowsingTrackingProtectionWhitelist : nsISupports
|
||||
{
|
||||
/**
|
||||
* Add a URI to the list of allowed tracking sites in Private Browsing mode
|
||||
* (essentially a tracking whitelist). This operation will cause the URI to
|
||||
* be registered if it does not currently exist. If it already exists, then
|
||||
* the operation is essentially a no-op.
|
||||
*
|
||||
* @param uri the uri to add to the list
|
||||
*/
|
||||
void addToAllowList(in nsIURI uri);
|
||||
|
||||
/**
|
||||
* Remove a URI from the list of allowed tracking sites in Private Browsing
|
||||
* mode (the tracking whitelist). If the URI is not already in the list,
|
||||
* then the operation is essentially a no-op.
|
||||
*
|
||||
* @param uri the uri to remove from the list
|
||||
*/
|
||||
void removeFromAllowList(in nsIURI uri);
|
||||
|
||||
/**
|
||||
* Check if a URI exists in the list of allowed tracking sites in Private
|
||||
* Browsing mode (the tracking whitelist).
|
||||
*
|
||||
* @param uri the uri to look for in the list
|
||||
*/
|
||||
bool existsInAllowList(in nsIURI uri);
|
||||
};
|
||||
|
||||
%{ C++
|
||||
#define NS_PBTRACKINGPROTECTIONWHITELIST_CONTRACTID "@mozilla.org/url-classifier/pbm-tp-whitelist;1"
|
||||
%}
|
@ -4,3 +4,5 @@ component {ca168834-cc00-48f9-b83c-fd018e58cae3} nsUrlClassifierListManager.js
|
||||
contract @mozilla.org/url-classifier/listmanager;1 {ca168834-cc00-48f9-b83c-fd018e58cae3}
|
||||
component {9111de73-9322-4bfc-8b65-2b727f3e6ec8} nsUrlClassifierHashCompleter.js
|
||||
contract @mozilla.org/url-classifier/hashcompleter;1 {9111de73-9322-4bfc-8b65-2b727f3e6ec8}
|
||||
component {a319b616-c45d-4037-8d86-01c592b5a9af} PrivateBrowsingTrackingProtectionWhitelist.js
|
||||
contract @mozilla.org/url-classifier/pbm-tp-whitelist;1 {a319b616-c45d-4037-8d86-01c592b5a9af}
|
||||
|
@ -68,15 +68,15 @@ module.exports = function makeDebugger({ findDebuggees, shouldAddNewGlobalAsDebu
|
||||
|
||||
dbg.uncaughtExceptionHook = reportDebuggerHookException;
|
||||
|
||||
dbg.onNewGlobalObject = global => {
|
||||
dbg.onNewGlobalObject = function(global) {
|
||||
if (shouldAddNewGlobalAsDebuggee(global)) {
|
||||
safeAddDebuggee(dbg, global);
|
||||
safeAddDebuggee(this, global);
|
||||
}
|
||||
};
|
||||
|
||||
dbg.addDebuggees = () => {
|
||||
for (let global of findDebuggees(dbg)) {
|
||||
safeAddDebuggee(dbg, global);
|
||||
dbg.addDebuggees = function() {
|
||||
for (let global of findDebuggees(this)) {
|
||||
safeAddDebuggee(this, global);
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -51,6 +51,18 @@ this.PrivateBrowsingUtils = {
|
||||
.QueryInterface(Ci.nsILoadContext);
|
||||
},
|
||||
|
||||
addToTrackingAllowlist(aURI) {
|
||||
let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"]
|
||||
.getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist);
|
||||
pbmtpWhitelist.addToAllowList(aURI);
|
||||
},
|
||||
|
||||
removeFromTrackingAllowlist(aURI) {
|
||||
let pbmtpWhitelist = Cc["@mozilla.org/url-classifier/pbm-tp-whitelist;1"]
|
||||
.getService(Ci.nsIPrivateBrowsingTrackingProtectionWhitelist);
|
||||
pbmtpWhitelist.removeFromAllowList(aURI);
|
||||
},
|
||||
|
||||
get permanentPrivateBrowsing() {
|
||||
try {
|
||||
return gTemporaryAutoStartMode ||
|
||||
|
@ -119,8 +119,8 @@ Installer.prototype = {
|
||||
if (install.linkedInstalls) {
|
||||
install.linkedInstalls.forEach(function(aInstall) {
|
||||
aInstall.addListener(this);
|
||||
// App disabled items are not compatible and so fail to install
|
||||
if (aInstall.addon.appDisabled)
|
||||
// Corrupt or incompatible items fail to install
|
||||
if (aInstall.state == AddonManager.STATE_DOWNLOAD_FAILED || aInstall.addon.appDisabled)
|
||||
failed.push(aInstall);
|
||||
else
|
||||
installs.push(aInstall);
|
||||
@ -132,7 +132,7 @@ Installer.prototype = {
|
||||
break;
|
||||
default:
|
||||
logger.warn("Download of " + install.sourceURI.spec + " in unexpected state " +
|
||||
install.state);
|
||||
install.state);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -5126,7 +5126,7 @@ AddonInstall.prototype = {
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this.loadManifest().then(() => {
|
||||
this.loadManifest(this.file).then(() => {
|
||||
XPIDatabase.getVisibleAddonForID(self.addon.id, function initLocalInstall_getVisibleAddon(aAddon) {
|
||||
self.existingAddon = aAddon;
|
||||
if (aAddon)
|
||||
@ -5162,6 +5162,10 @@ AddonInstall.prototype = {
|
||||
logger.warn("Invalid XPI", message);
|
||||
this.state = AddonManager.STATE_DOWNLOAD_FAILED;
|
||||
this.error = error;
|
||||
AddonManagerPrivate.callInstallListeners("onNewInstall",
|
||||
self.listeners,
|
||||
self.wrapper);
|
||||
|
||||
aCallback(this);
|
||||
});
|
||||
},
|
||||
@ -5337,6 +5341,45 @@ AddonInstall.prototype = {
|
||||
this.addon.releaseNotesURI = this.releaseNotesURI.spec;
|
||||
},
|
||||
|
||||
/**
|
||||
* Fills out linkedInstalls with AddonInstall instances for the other files
|
||||
* in a multi-package XPI.
|
||||
*
|
||||
* @param aFiles
|
||||
* An array of { entryName, file } for each remaining file from the
|
||||
* multi-package XPI.
|
||||
*/
|
||||
_createLinkedInstalls: Task.async(function* AI_createLinkedInstalls(aFiles) {
|
||||
if (aFiles.length == 0)
|
||||
return;
|
||||
|
||||
// Create new AddonInstall instances for every remaining file
|
||||
if (!this.linkedInstalls)
|
||||
this.linkedInstalls = [];
|
||||
|
||||
for (let { entryName, file } of aFiles) {
|
||||
logger.debug("Creating linked install from " + entryName);
|
||||
let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
|
||||
|
||||
// Make the new install own its temporary file
|
||||
install.ownsTempFile = true;
|
||||
|
||||
this.linkedInstalls.push(install);
|
||||
|
||||
// If one of the internal XPIs was multipackage then move its linked
|
||||
// installs to the outer install
|
||||
if (install.linkedInstalls) {
|
||||
this.linkedInstalls.push(...install.linkedInstalls);
|
||||
install.linkedInstalls = null;
|
||||
}
|
||||
|
||||
install.sourceURI = this.sourceURI;
|
||||
install.releaseNotesURI = this.releaseNotesURI;
|
||||
if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
|
||||
install.updateAddonURIs();
|
||||
}
|
||||
}),
|
||||
|
||||
/**
|
||||
* Loads add-on manifests from a multi-package XPI file. Each of the
|
||||
* XPI and JAR files contained in the XPI will be extracted. Any that
|
||||
@ -5347,97 +5390,58 @@ AddonInstall.prototype = {
|
||||
* @param aZipReader
|
||||
* An open nsIZipReader for the multi-package XPI's files. This will
|
||||
* be closed before this method returns.
|
||||
* @param aCallback
|
||||
* A function to call when all of the add-on manifests have been
|
||||
* loaded. Because this loadMultipackageManifests is an internal API
|
||||
* we don't exception-wrap this callback
|
||||
*/
|
||||
_loadMultipackageManifests: Task.async(function* AI_loadMultipackageManifests(aZipReader) {
|
||||
let files = [];
|
||||
let entries = aZipReader.findEntries("(*.[Xx][Pp][Ii]|*.[Jj][Aa][Rr])");
|
||||
while (entries.hasMore()) {
|
||||
let entryName = entries.getNext();
|
||||
var target = getTemporaryFile();
|
||||
let file = getTemporaryFile();
|
||||
try {
|
||||
aZipReader.extract(entryName, target);
|
||||
files.push(target);
|
||||
aZipReader.extract(entryName, file);
|
||||
files.push({ entryName, file });
|
||||
}
|
||||
catch (e) {
|
||||
logger.warn("Failed to extract " + entryName + " from multi-package " +
|
||||
"XPI", e);
|
||||
target.remove(false);
|
||||
file.remove(false);
|
||||
}
|
||||
}
|
||||
|
||||
aZipReader.close();
|
||||
|
||||
if (files.length == 0) {
|
||||
throw new Error("Multi-package XPI does not contain any packages " +
|
||||
"to install");
|
||||
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
|
||||
"Multi-package XPI does not contain any packages to install"]);
|
||||
}
|
||||
|
||||
let addon = null;
|
||||
|
||||
// Find the first file that has a valid install manifest and use it for
|
||||
// Find the first file that is a valid install and use it for
|
||||
// the add-on that this AddonInstall instance will install.
|
||||
while (files.length > 0) {
|
||||
for (let { entryName, file } of files) {
|
||||
this.removeTemporaryFile();
|
||||
this.file = files.shift();
|
||||
this.ownsTempFile = true;
|
||||
try {
|
||||
addon = yield loadManifestFromZipFile(this.file);
|
||||
break;
|
||||
yield this.loadManifest(file);
|
||||
logger.debug("Base multi-package XPI install came from " + entryName);
|
||||
this.file = file;
|
||||
this.ownsTempFile = true;
|
||||
|
||||
yield this._createLinkedInstalls(files.filter(f => f.file != file));
|
||||
return;
|
||||
}
|
||||
catch (e) {
|
||||
logger.warn(this.file.leafName + " cannot be installed from multi-package " +
|
||||
"XPI", e);
|
||||
// _createLinkedInstalls will log errors when it tries to process this
|
||||
// file
|
||||
}
|
||||
}
|
||||
|
||||
if (!addon) {
|
||||
// No valid add-on was found
|
||||
return;
|
||||
}
|
||||
// No valid add-on was found, delete all the temporary files
|
||||
for (let { file } of files)
|
||||
file.remove(true);
|
||||
|
||||
this.addon = addon;
|
||||
|
||||
this.updateAddonURIs();
|
||||
|
||||
this.addon._install = this;
|
||||
this.name = this.addon.selectedLocale.name;
|
||||
this.type = this.addon.type;
|
||||
this.version = this.addon.version;
|
||||
|
||||
// Setting the iconURL to something inside the XPI locks the XPI and
|
||||
// makes it impossible to delete on Windows.
|
||||
//let newIcon = createWrapper(this.addon).iconURL;
|
||||
//if (newIcon)
|
||||
// this.iconURL = newIcon;
|
||||
|
||||
// Create new AddonInstall instances for every remaining file
|
||||
if (files.length > 0) {
|
||||
this.linkedInstalls = [];
|
||||
let self = this;
|
||||
for (let file of files) {
|
||||
let install = yield new Promise(resolve => AddonInstall.createInstall(resolve, file));
|
||||
|
||||
// Ignore bad add-ons (createInstall will have logged the error)
|
||||
if (install.state == AddonManager.STATE_DOWNLOAD_FAILED) {
|
||||
// Manually remove the temporary file
|
||||
file.remove(true);
|
||||
}
|
||||
else {
|
||||
// Make the new install own its temporary file
|
||||
install.ownsTempFile = true;
|
||||
|
||||
self.linkedInstalls.push(install)
|
||||
|
||||
install.sourceURI = self.sourceURI;
|
||||
install.releaseNotesURI = self.releaseNotesURI;
|
||||
install.updateAddonURIs();
|
||||
}
|
||||
}
|
||||
}
|
||||
return Promise.reject([AddonManager.ERROR_CORRUPT_FILE,
|
||||
"Multi-package XPI does not contain any valid packages to install"]);
|
||||
}),
|
||||
|
||||
/**
|
||||
@ -5449,11 +5453,11 @@ AddonInstall.prototype = {
|
||||
* @throws if the add-on does not contain a valid install manifest or the
|
||||
* XPI is incorrectly signed
|
||||
*/
|
||||
loadManifest: Task.async(function* AI_loadManifest() {
|
||||
loadManifest: Task.async(function* AI_loadManifest(file) {
|
||||
let zipreader = Cc["@mozilla.org/libjar/zip-reader;1"].
|
||||
createInstance(Ci.nsIZipReader);
|
||||
try {
|
||||
zipreader.open(this.file);
|
||||
zipreader.open(file);
|
||||
}
|
||||
catch (e) {
|
||||
zipreader.close();
|
||||
@ -5771,7 +5775,7 @@ AddonInstall.prototype = {
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this.loadManifest().then(() => {
|
||||
this.loadManifest(this.file).then(() => {
|
||||
if (self.addon.isCompatible) {
|
||||
self.downloadCompleted();
|
||||
}
|
||||
@ -5860,9 +5864,10 @@ AddonInstall.prototype = {
|
||||
self.install();
|
||||
|
||||
if (self.linkedInstalls) {
|
||||
self.linkedInstalls.forEach(function(aInstall) {
|
||||
aInstall.install();
|
||||
});
|
||||
for (let install of self.linkedInstalls) {
|
||||
if (install.state == AddonManager.STATE_DOWNLOADED)
|
||||
install.install();
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
@ -0,0 +1 @@
|
||||
This isn't a valid zip file.
|
@ -0,0 +1 @@
|
||||
This isn't a valid zip file.
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- A multi-package XPI -->
|
||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
|
||||
<Description about="urn:mozilla:install-manifest">
|
||||
<em:type>32</em:type>
|
||||
</Description>
|
||||
</RDF>
|
@ -0,0 +1,10 @@
|
||||
<?xml version="1.0"?>
|
||||
|
||||
<!-- A multi-package XPI -->
|
||||
<RDF xmlns="http://www.w3.org/1999/02/22-rdf-syntax-ns#"
|
||||
xmlns:em="http://www.mozilla.org/2004/em-rdf#">
|
||||
|
||||
<Description about="urn:mozilla:install-manifest">
|
||||
<em:type>32</em:type>
|
||||
</Description>
|
||||
</RDF>
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -132,6 +132,32 @@
|
||||
<featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
|
||||
</gfxBlacklistEntry>
|
||||
|
||||
<gfxBlacklistEntry>
|
||||
<os>All</os>
|
||||
<vendor>0xabcd</vendor>
|
||||
<versionRange minVersion="42.0" maxVersion="13.0b2"/>
|
||||
<devices>
|
||||
<device>0x2783</device>
|
||||
<device>0x1234</device>
|
||||
<device>0x2782</device>
|
||||
</devices>
|
||||
<feature> WEBRTC_HW_ACCELERATION_ENCODE </feature>
|
||||
<featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
|
||||
</gfxBlacklistEntry>
|
||||
|
||||
<gfxBlacklistEntry>
|
||||
<os>All</os>
|
||||
<vendor>0xabcd</vendor>
|
||||
<versionRange minVersion="42.0" maxVersion="13.0b2"/>
|
||||
<devices>
|
||||
<device>0x2783</device>
|
||||
<device>0x1234</device>
|
||||
<device>0x2782</device>
|
||||
</devices>
|
||||
<feature> WEBRTC_HW_ACCELERATION_DECODE </feature>
|
||||
<featureStatus> BLOCKED_DRIVER_VERSION </featureStatus>
|
||||
</gfxBlacklistEntry>
|
||||
|
||||
<gfxBlacklistEntry>
|
||||
<os>All</os>
|
||||
<vendor>0xabcd</vendor>
|
||||
|
@ -1108,9 +1108,13 @@ const AddonListener = {
|
||||
const InstallListener = {
|
||||
onNewInstall: function(install) {
|
||||
if (install.state != AddonManager.STATE_DOWNLOADED &&
|
||||
install.state != AddonManager.STATE_DOWNLOAD_FAILED &&
|
||||
install.state != AddonManager.STATE_AVAILABLE)
|
||||
do_throw("Bad install state " + install.state);
|
||||
do_check_eq(install.error, 0);
|
||||
if (install.state != AddonManager.STATE_DOWNLOAD_FAILED)
|
||||
do_check_eq(install.error, 0);
|
||||
else
|
||||
do_check_neq(install.error, 0);
|
||||
do_check_eq("onNewInstall", getExpectedInstall());
|
||||
return check_test_completed(arguments);
|
||||
},
|
||||
|
@ -107,6 +107,12 @@ function run_test() {
|
||||
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION);
|
||||
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
|
||||
|
||||
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_ENCODE);
|
||||
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
|
||||
|
||||
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_WEBRTC_HW_ACCELERATION_DECODE);
|
||||
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
|
||||
|
||||
status = gfxInfo.getFeatureStatus(Ci.nsIGfxInfo.FEATURE_DIRECT3D_11_LAYERS);
|
||||
do_check_eq(status, Ci.nsIGfxInfo.FEATURE_STATUS_OK);
|
||||
|
||||
|
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user