Merge m-c to b2ginbound, a=merge

This commit is contained in:
Wes Kocher 2015-06-10 18:39:02 -07:00
commit 68f351b859
697 changed files with 11583 additions and 5962 deletions

View File

@ -34,7 +34,7 @@
#endif
#ifdef MOZ_WIDGET_GONK
#include "GonkDisplay.h"
#include "BootAnimation.h"
#endif
#include "BinaryPath.h"
@ -148,8 +148,8 @@ static int do_main(int argc, char* argv[])
}
#ifdef MOZ_WIDGET_GONK
/* Called to start the boot animation */
(void) mozilla::GetGonkDisplay();
/* Start boot animation */
mozilla::StartBootAnimation();
#endif
if (appini) {

View File

@ -130,7 +130,16 @@ this.AboutServiceWorkers = {
self.sendError(message.id, "MissingScope");
return;
}
gServiceWorkerManager.softUpdate(message.scope);
if (!message.principal ||
!message.principal.originAttributes) {
// XXX This will always error until bug 1171915 is fixed.
self.sendError(message.id, "MissingOriginAttributes");
return;
}
gServiceWorkerManager.propagateSoftUpdate({},
message.scope);
self.sendResult(message.id, true);
break;
@ -166,7 +175,7 @@ this.AboutServiceWorkers = {
Ci.nsIServiceWorkerUnregisterCallback
])
};
gServiceWorkerManager.unregister(principal,
gServiceWorkerManager.propagateUnregister(principal,
serviceWorkerUnregisterCallback,
message.scope);
break;

View File

@ -128,12 +128,15 @@ add_test(function test_swm() {
"SWM.getAllRegistrations exists");
do_check_true(typeof gServiceWorkerManager.getAllRegistrations == "function",
"SWM.getAllRegistrations is a function");
do_check_true(gServiceWorkerManager.softUpdate, "SWM.softUpdate exists");
do_check_true(typeof gServiceWorkerManager.softUpdate == "function",
"SWM.softUpdate is a function");
do_check_true(gServiceWorkerManager.unregister, "SWM.unregister exists");
do_check_true(typeof gServiceWorkerManager.unregister == "function",
"SWM.unregister exists");
do_check_true(gServiceWorkerManager.propagateSoftUpdate,
"SWM.propagateSoftUpdate exists");
do_check_true(typeof gServiceWorkerManager.propagateSoftUpdate == "function",
"SWM.propagateSoftUpdate is a function");
do_check_true(gServiceWorkerManager.propagateUnregister,
"SWM.propagateUnregister exists");
do_check_true(typeof gServiceWorkerManager.propagateUnregister == "function",
"SWM.propagateUnregister exists");
run_next_test();
});

View File

@ -270,7 +270,7 @@ pref("general.autoScroll", true);
// At startup, check if we're the default browser and prompt user if not.
pref("browser.shell.checkDefaultBrowser", true);
pref("browser.shell.shortcutFavicons",true);
pref("browser.shell.isSetAsDefaultBrowser", false);
pref("browser.shell.mostRecentDateSetAsDefault", "");
// 0 = blank, 1 = home (browser.startup.homepage), 2 = last visited page, 3 = resume previous browser session
// The behavior of option 3 is detailed at: http://wiki.mozilla.org/Session_Restore
@ -1462,6 +1462,7 @@ pref("devtools.performance.ui.flatten-tree-recursion", true);
pref("devtools.performance.ui.show-platform-data", false);
pref("devtools.performance.ui.show-idle-blocks", true);
pref("devtools.performance.ui.enable-memory", false);
pref("devtools.performance.ui.enable-allocations", false);
pref("devtools.performance.ui.enable-framerate", true);
pref("devtools.performance.ui.show-jit-optimizations", false);
@ -1487,6 +1488,15 @@ pref("devtools.netmonitor.panes-network-details-height", 450);
pref("devtools.netmonitor.statistics", true);
pref("devtools.netmonitor.filters", "[\"all\"]");
// The default Network monitor HAR export setting
pref("devtools.netmonitor.har.defaultLogDir", "");
pref("devtools.netmonitor.har.defaultFileName", "archive");
pref("devtools.netmonitor.har.jsonp", false);
pref("devtools.netmonitor.har.jsonpCallback", "");
pref("devtools.netmonitor.har.includeResponseBodies", true);
pref("devtools.netmonitor.har.compress", false);
pref("devtools.netmonitor.har.forceExport", false);
// Enable the Tilt inspector
pref("devtools.tilt.enabled", true);
pref("devtools.tilt.intro_transition", true);

View File

@ -112,6 +112,12 @@
.addEventListener('click', function togglePanelVisibility() {
var panel = document.getElementById('certificateErrorReportingPanel');
toggleDisplay(panel);
if (panel.style.display == "block") {
// send event to trigger telemetry ping
var event = new CustomEvent("AboutNetErrorUIExpanded", {bubbles:true});
document.dispatchEvent(event);
}
});
}

View File

@ -3,8 +3,6 @@
%brandDTD;
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd" >
%browserDTD;
<!ENTITY % browserPocketDTD SYSTEM "chrome://browser/content/browser-pocket.dtd" >
%browserPocketDTD;
<!ENTITY % baseMenuDTD SYSTEM "chrome://browser/locale/baseMenuOverlay.dtd" >
%baseMenuDTD;
<!ENTITY % charsetDTD SYSTEM "chrome://global/locale/charsetMenu.dtd" >

View File

@ -7,14 +7,17 @@ var FullScreen = {
_XULNS: "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul",
_MESSAGES: [
"DOMFullscreen:Entered",
"DOMFullscreen:Request",
"DOMFullscreen:NewOrigin",
"DOMFullscreen:Exited"
"DOMFullscreen:Exited",
],
init: function() {
// called when we go into full screen, even if initiated by a web page script
window.addEventListener("fullscreen", this, true);
window.addEventListener("MozDOMFullscreen:Entered", this,
/* useCapture */ true,
/* wantsUntrusted */ false);
window.addEventListener("MozDOMFullscreen:Exited", this,
/* useCapture */ true,
/* wantsUntrusted */ false);
@ -33,13 +36,9 @@ var FullScreen = {
this.cleanup();
},
toggle: function (event) {
toggle: function () {
var enterFS = window.fullScreen;
// We get the fullscreen event _before_ the window transitions into or out of FS mode.
if (event && event.type == "fullscreen")
enterFS = !enterFS;
// Toggle the View:FullScreen command, which controls elements like the
// fullscreen menuitem, and menubars.
let fullscreenCommand = document.getElementById("View:FullScreen");
@ -106,12 +105,41 @@ var FullScreen = {
}
break;
case "fullscreen":
this.toggle(event);
this.toggle();
break;
case "transitionend":
if (event.propertyName == "opacity")
this.cancelWarning();
break;
case "MozDOMFullscreen:Entered": {
// The original target is the element which requested the DOM
// fullscreen. If we were entering DOM fullscreen for a remote
// browser, this element would be that browser element, which
// was the parameter of `remoteFrameFullscreenChanged` call.
// If the fullscreen request was initiated from an in-process
// browser, we need to get its corresponding browser element.
let originalTarget = event.originalTarget;
let browser;
if (this._isBrowser(originalTarget)) {
browser = originalTarget;
} else {
let topWin = originalTarget.ownerDocument.defaultView.top;
browser = gBrowser.getBrowserForContentWindow(topWin);
if (!browser) {
document.mozCancelFullScreen();
break;
}
}
this.enterDomFullscreen(browser);
// If it is a remote browser, send a message to ask the content
// to enter fullscreen state. We don't need to do so if it is an
// in-process browser, since all related document should have
// entered fullscreen state at this point.
if (this._isRemoteBrowser(browser)) {
browser.messageManager.sendAsyncMessage("DOMFullscreen:Entered");
}
break;
}
case "MozDOMFullscreen:Exited":
this.cleanupDomFullscreen();
break;
@ -121,16 +149,8 @@ var FullScreen = {
receiveMessage: function(aMessage) {
let browser = aMessage.target;
switch (aMessage.name) {
case "DOMFullscreen:Entered": {
// If we're a multiprocess browser, then the request to enter
// fullscreen did not bubble up to the root browser document -
// it stopped at the root of the content document. That means
// we have to kick off the switch to fullscreen here at the
// operating system level in the parent process ourselves.
if (this._isRemoteBrowser(browser)) {
case "DOMFullscreen:Request": {
this._windowUtils.remoteFrameFullscreenChanged(browser);
}
this.enterDomFullscreen(browser);
break;
}
case "DOMFullscreen:NewOrigin": {
@ -192,7 +212,7 @@ var FullScreen = {
},
cleanup: function () {
if (window.fullScreen) {
if (!window.fullScreen) {
MousePosTracker.removeListener(this);
document.removeEventListener("keypress", this._keyToggleCallback, false);
document.removeEventListener("popupshown", this._setPopupOpen, false);
@ -220,6 +240,13 @@ var FullScreen = {
.broadcastAsyncMessage("DOMFullscreen:CleanUp");
},
_isBrowser: function (aNode) {
if (aNode.tagName != "xul:browser") {
return false;
}
let systemPrincipal = Services.scriptSecurityManager.getSystemPrincipal();
return aNode.nodePrincipal == systemPrincipal;
},
_isRemoteBrowser: function (aBrowser) {
return gMultiProcessBrowser && aBrowser.getAttribute("remote") == "true";
},

View File

@ -543,7 +543,8 @@
observes="devtoolsMenuBroadcaster_BrowserToolbox"
accesskey="&browserToolboxMenu.accesskey;"/>
<menuitem id="menu_browserContentToolbox"
observes="devtoolsMenuBroadcaster_BrowserContentToolbox"/>
observes="devtoolsMenuBroadcaster_BrowserContentToolbox"
accesskey="&browserContentToolboxMenu.accesskey;" />
<menuitem id="menu_browserConsole"
observes="devtoolsMenuBroadcaster_BrowserConsole"
accesskey="&browserConsoleCmd.accesskey;"/>

View File

@ -1563,25 +1563,6 @@ let BookmarkingUI = {
updatePocketItemVisibility: function BUI_updatePocketItemVisibility(prefix) {
let hidden = !CustomizableUI.getPlacementOfWidget("pocket-button");
if (!hidden) {
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale("browser");
if (locale != "en-US") {
if (locale == "ja-JP-mac")
locale = "ja";
let url = "chrome://browser/content/browser-pocket-" + locale + ".properties";
let bundle = Services.strings.createBundle(url);
let item = document.getElementById(prefix + "pocket");
try {
item.setAttribute("label", bundle.GetStringFromName("pocketMenuitem.label"));
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that
// case, the item will retain the browser-pocket.dtd en-US string that
// it has in the markup.
}
}
}
document.getElementById(prefix + "pocket").hidden = hidden;
document.getElementById(prefix + "pocketSeparator").hidden = hidden;
},

View File

@ -1,16 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This is a temporary file, later versions of Firefox will use
# browser.properties in the usual L10N location.
pocket-button.label = Pocket
pocket-button.tooltiptext = Bei Pocket speichern
# From browser-pocket.dtd
saveToPocketCmd.label = Seite bei Pocket speichern
saveToPocketCmd.accesskey = k
saveLinkToPocketCmd.label = Link bei Pocket speichern
saveLinkToPocketCmd.accesskey = o
pocketMenuitem.label = Pocket-Liste anzeigen

View File

@ -1,16 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This is a temporary file, later versions of Firefox will use
# browser.properties in the usual L10N location.
pocket-button.label = Pocket
pocket-button.tooltiptext = Guardar en Pocket
# From browser-pocket.dtd
saveToPocketCmd.label = Guardar página en Pocket
saveToPocketCmd.accesskey = k
saveLinkToPocketCmd.label = Guardar enlace en Pocket
saveLinkToPocketCmd.accesskey = k
pocketMenuitem.label = Ver lista de Pocket

View File

@ -1,16 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This is a temporary file, later versions of Firefox will use
# browser.properties in the usual L10N location.
pocket-button.label = Pocket
pocket-button.tooltiptext = Pocket に保存
# From browser-pocket.dtd
saveToPocketCmd.label = Pocket にページを保存
saveToPocketCmd.accesskey = k
saveLinkToPocketCmd.label = Pocket にリンクを保存
saveLinkToPocketCmd.accesskey = o
pocketMenuitem.label = Pocket のマイリストを表示

View File

@ -1,16 +0,0 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
# This is a temporary file, later versions of Firefox will use
# browser.properties in the usual L10N location.
pocket-button.label = Pocket
pocket-button.tooltiptext = Сохранить в Pocket
# From browser-pocket.dtd
saveToPocketCmd.label = Сохранить страницу в Pocket
saveToPocketCmd.accesskey = х
saveLinkToPocketCmd.label = Сохранить ссылку в Pocket
saveLinkToPocketCmd.accesskey = а
pocketMenuitem.label = Показать список Pocket

View File

@ -1,12 +0,0 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<!-- This is a temporary file and not meant for localization; later versions
- of Firefox include these strings in browser.dtd -->
<!ENTITY saveToPocketCmd.label "Save Page to Pocket">
<!ENTITY saveToPocketCmd.accesskey "k">
<!ENTITY saveLinkToPocketCmd.label "Save Link to Pocket">
<!ENTITY saveLinkToPocketCmd.accesskey "o">
<!ENTITY pocketMenuitem.label "View Pocket List">

View File

@ -1291,3 +1291,7 @@ toolbarpaletteitem[place="palette"][hidden] {
.popup-notification-footer[popupid="bad-content"][trackingblockdisabled] {
display: block;
}
#login-fill-doorhanger:not([inDetailView]) > #login-fill-clickcapturer {
pointer-events: none;
}

View File

@ -220,9 +220,6 @@ XPCOMUtils.defineLazyModuleGetter(this, "SitePermissions",
XPCOMUtils.defineLazyModuleGetter(this, "SessionStore",
"resource:///modules/sessionstore/SessionStore.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "TabState",
"resource:///modules/sessionstore/TabState.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "fxAccounts",
"resource://gre/modules/FxAccounts.jsm");
@ -921,22 +918,7 @@ function _loadURIWithFlags(browser, uri, params) {
// process
function LoadInOtherProcess(browser, loadOptions, historyIndex = -1) {
let tab = gBrowser.getTabForBrowser(browser);
// Flush the tab state before getting it
TabState.flush(browser);
let tabState = JSON.parse(SessionStore.getTabState(tab));
if (historyIndex < 0) {
tabState.userTypedValue = null;
// Tell session history the new page to load
SessionStore._restoreTabAndLoad(tab, JSON.stringify(tabState), loadOptions);
}
else {
// Update the history state to point to the requested index
tabState.index = historyIndex + 1;
// SessionStore takes care of setting the browser remoteness before restoring
// history into it.
SessionStore.setTabState(tab, JSON.stringify(tabState));
}
SessionStore.navigateAndRestore(tab, loadOptions, historyIndex);
}
// Called when a docshell has attempted to load a page in an incorrect process.
@ -2692,6 +2674,12 @@ let gMenuButtonUpdateBadge = {
}
};
// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
const TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED = 2;
const TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED = 3;
const TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND = 4;
const TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND = 5;
/**
* Handle command events bubbling up from error page content
* or from about:newtab or from remote error pages that invoke
@ -2705,6 +2693,7 @@ let BrowserOnClick = {
mm.addMessageListener("Browser:EnableOnlineMode", this);
mm.addMessageListener("Browser:SendSSLErrorReport", this);
mm.addMessageListener("Browser:SetSSLErrorReportAuto", this);
mm.addMessageListener("Browser:SSLErrorReportTelemetry", this);
},
uninit: function () {
@ -2714,6 +2703,7 @@ let BrowserOnClick = {
mm.removeMessageListener("Browser:EnableOnlineMode", this);
mm.removeMessageListener("Browser:SendSSLErrorReport", this);
mm.removeMessageListener("Browser:SetSSLErrorReportAuto", this);
mm.removeMessageListener("Browser:SSLErrorReportTelemetry", this);
},
handleEvent: function (event) {
@ -2760,6 +2750,16 @@ let BrowserOnClick = {
break;
case "Browser:SetSSLErrorReportAuto":
Services.prefs.setBoolPref("security.ssl.errorReporting.automatic", msg.json.automatic);
let bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_UNCHECKED;
if (msg.json.automatic) {
bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_CHECKED;
}
Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI").add(bin);
break;
case "Browser:SSLErrorReportTelemetry":
let reportStatus = msg.data.reportStatus;
Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI")
.add(reportStatus);
break;
}
},
@ -2781,6 +2781,12 @@ let BrowserOnClick = {
return;
}
let bin = TLS_ERROR_REPORT_TELEMETRY_MANUAL_SEND;
if (Services.prefs.getBoolPref("security.ssl.errorReporting.automatic")) {
bin = TLS_ERROR_REPORT_TELEMETRY_AUTO_SEND;
}
Services.telemetry.getHistogramById("TLS_ERROR_REPORT_UI").add(bin);
let serhelper = Cc["@mozilla.org/network/serialization-helper;1"]
.getService(Ci.nsISerializationHelper);
let transportSecurityInfo = serhelper.deserializeObject(securityInfo);
@ -6579,23 +6585,6 @@ var gIdentityHandler = {
_mode : "unknownIdentity",
// smart getters
get _encryptionLabel () {
delete this._encryptionLabel;
this._encryptionLabel = {};
this._encryptionLabel[this.IDENTITY_MODE_DOMAIN_VERIFIED] =
gNavigatorBundle.getString("identity.encrypted2");
this._encryptionLabel[this.IDENTITY_MODE_IDENTIFIED] =
gNavigatorBundle.getString("identity.encrypted2");
this._encryptionLabel[this.IDENTITY_MODE_UNKNOWN] =
gNavigatorBundle.getString("identity.unencrypted");
this._encryptionLabel[this.IDENTITY_MODE_MIXED_DISPLAY_LOADED] =
gNavigatorBundle.getString("identity.broken_loaded");
this._encryptionLabel[this.IDENTITY_MODE_MIXED_ACTIVE_LOADED] =
gNavigatorBundle.getString("identity.mixed_active_loaded2");
this._encryptionLabel[this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED] =
gNavigatorBundle.getString("identity.broken_loaded");
return this._encryptionLabel;
},
get _identityPopup () {
delete this._identityPopup;
return this._identityPopup = document.getElementById("identity-popup");
@ -6609,11 +6598,6 @@ var gIdentityHandler = {
return this._identityPopupContentBox =
document.getElementById("identity-popup-content-box");
},
get _identityPopupChromeLabel () {
delete this._identityPopupChromeLabel;
return this._identityPopupChromeLabel =
document.getElementById("identity-popup-chromeLabel");
},
get _identityPopupContentHost () {
delete this._identityPopupContentHost;
return this._identityPopupContentHost =
@ -6634,11 +6618,6 @@ var gIdentityHandler = {
return this._identityPopupContentVerif =
document.getElementById("identity-popup-content-verifier");
},
get _identityPopupEncLabel () {
delete this._identityPopupEncLabel;
return this._identityPopupEncLabel =
document.getElementById("identity-popup-encryption-label");
},
get _identityIconLabel () {
delete this._identityIconLabel;
return this._identityIconLabel = document.getElementById("identity-icon-label");
@ -6956,25 +6935,32 @@ var gIdentityHandler = {
this._identityPopup.className = newMode;
this._identityPopupContentBox.className = newMode;
// Set the static strings up front
this._identityPopupEncLabel.textContent = this._encryptionLabel[newMode];
// Initialize the optional strings to empty values
let supplemental = "";
let verifier = "";
let host = "";
let owner = "";
if (newMode == this.IDENTITY_MODE_CHROMEUI) {
let brandBundle = document.getElementById("bundle_brand");
host = brandBundle.getString("brandFullName");
} else {
try {
host = this.getEffectiveHost();
} catch (e) {
// Some URIs might have no hosts.
host = this._lastUri.specIgnoringRef;
}
}
switch (newMode) {
case this.IDENTITY_MODE_DOMAIN_VERIFIED:
host = this.getEffectiveHost();
verifier = this._identityBox.tooltipText;
break;
case this.IDENTITY_MODE_IDENTIFIED: {
// If it's identified, then we can populate the dialog with credentials
let iData = this.getIdentityData();
host = this.getEffectiveHost();
owner = iData.subjectOrg;
host = owner = iData.subjectOrg;
verifier = this._identityBox.tooltipText;
// Build an appropriate supplemental block out of whatever location data we have
@ -6987,17 +6973,21 @@ var gIdentityHandler = {
supplemental += iData.state;
else if (iData.country) // Country only
supplemental += iData.country;
break; }
case this.IDENTITY_MODE_CHROMEUI: {
let brandBundle = document.getElementById("bundle_brand");
let brandShortName = brandBundle.getString("brandShortName");
this._identityPopupChromeLabel.textContent = gNavigatorBundle.getFormattedString("identity.chrome",
[brandShortName]);
break; }
break;
}
case this.IDENTITY_MODE_MIXED_DISPLAY_LOADED:
case this.IDENTITY_MODE_MIXED_DISPLAY_LOADED_ACTIVE_BLOCKED:
supplemental = gNavigatorBundle.getString("identity.broken_loaded");
break;
case this.IDENTITY_MODE_MIXED_ACTIVE_LOADED:
supplemental = gNavigatorBundle.getString("identity.mixed_active_loaded2");
break;
}
// Push the appropriate strings out to the UI
this._identityPopupContentHost.textContent = host;
// Push the appropriate strings out to the UI. Need to use |value| for the
// host as it's a <label> that will be cropped if too long. Using
// |textContent| would simply wrap the value.
this._identityPopupContentHost.value = host;
this._identityPopupContentOwner.textContent = owner;
this._identityPopupContentSupp.textContent = supplemental;
this._identityPopupContentVerif.textContent = verifier;

View File

@ -177,11 +177,18 @@ Cc["@mozilla.org/eventlistenerservice;1"]
.getService(Ci.nsIEventListenerService)
.addSystemEventListener(global, "contextmenu", handleContentContextMenu, false);
// Values for telemtery bins: see TLS_ERROR_REPORT_UI in Histograms.json
const TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN = 0;
const TLS_ERROR_REPORT_TELEMETRY_EXPANDED = 1;
const TLS_ERROR_REPORT_TELEMETRY_SUCCESS = 6;
const TLS_ERROR_REPORT_TELEMETRY_FAILURE = 7;
let AboutNetErrorListener = {
init: function(chromeGlobal) {
chromeGlobal.addEventListener('AboutNetErrorLoad', this, false, true);
chromeGlobal.addEventListener('AboutNetErrorSetAutomatic', this, false, true);
chromeGlobal.addEventListener('AboutNetErrorSendReport', this, false, true);
chromeGlobal.addEventListener('AboutNetErrorUIExpanded', this, false, true);
},
get isAboutNetError() {
@ -203,6 +210,10 @@ let AboutNetErrorListener = {
case "AboutNetErrorSendReport":
this.onSendReport(aEvent);
break;
case "AboutNetErrorUIExpanded":
sendAsyncMessage("Browser:SSLErrorReportTelemetry",
{reportStatus: TLS_ERROR_REPORT_TELEMETRY_EXPANDED});
break;
}
},
@ -215,6 +226,10 @@ let AboutNetErrorListener = {
})
}
));
sendAsyncMessage("Browser:SSLErrorReportTelemetry",
{reportStatus: TLS_ERROR_REPORT_TELEMETRY_UI_SHOWN});
if (automatic) {
this.onSendReport(evt);
}
@ -259,11 +274,15 @@ let AboutNetErrorListener = {
// show the retry button
retryBtn.style.removeProperty("display");
reportSendingMsg.style.display = "none";
sendAsyncMessage("Browser:SSLErrorReportTelemetry",
{reportStatus: TLS_ERROR_REPORT_TELEMETRY_FAILURE});
break;
case "complete":
// Show a success indicator
reportSentMsg.style.removeProperty("display");
reportSendingMsg.style.display = "none";
sendAsyncMessage("Browser:SSLErrorReportTelemetry",
{reportStatus: TLS_ERROR_REPORT_TELEMETRY_SUCCESS});
break;
}
}
@ -284,7 +303,7 @@ let AboutNetErrorListener = {
sendAsyncMessage("Browser:SendSSLErrorReport", {
elementId: evt.target.id,
documentURI: contentDoc.documentURI,
location: contentDoc.location,
location: {hostname: contentDoc.location.hostname, port: contentDoc.location.port},
securityInfo: serializedSecurityInfo
});
}

View File

@ -198,29 +198,6 @@ nsContextMenu.prototype = {
(targetURI.schemeIs("about") && ReaderMode.getOriginalUrl(targetURI.spec)));
canPocket = canPocket && window.gBrowser && this.browser.getTabBrowser() == window.gBrowser;
if (canPocket) {
let locale = Cc["@mozilla.org/chrome/chrome-registry;1"].
getService(Ci.nsIXULChromeRegistry).
getSelectedLocale("browser");
if (locale != "en-US") {
if (locale == "ja-JP-mac")
locale = "ja";
let url = "chrome://browser/content/browser-pocket-" + locale + ".properties";
let bundle = Services.strings.createBundle(url);
let saveToPocketItem = document.getElementById("context-pocket");
let saveLinkToPocketItem = document.getElementById("context-savelinktopocket");
try {
saveToPocketItem.setAttribute("label", bundle.GetStringFromName("saveToPocketCmd.label"));
saveToPocketItem.setAttribute("accesskey", bundle.GetStringFromName("saveToPocketCmd.accesskey"));
saveLinkToPocketItem.setAttribute("label", bundle.GetStringFromName("saveLinkToPocketCmd.label"));
saveLinkToPocketItem.setAttribute("accesskey", bundle.GetStringFromName("saveLinkToPocketCmd.accesskey"));
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that
// case, the item will retain the browser-pocket.dtd en-US string that
// it has in the markup.
}
}
}
this.showItem("context-pocket", canPocket && showSaveCurrentPageToPocket);
let showSaveLinkToPocket = canPocket && !showSaveCurrentPageToPocket &&
(this.onSaveableLink || this.onPlainTextLink);

View File

@ -62,12 +62,22 @@
</popupnotificationcontent>
</popupnotification>
<vbox id="login-fill-doorhanger" hidden="true">
<stack id="login-fill-doorhanger" hidden="true">
<vbox id="login-fill-mainview">
<description id="login-fill-testing"
value="Thanks for testing the login fill doorhanger!"/>
<textbox id="login-fill-filter"/>
<richlistbox id="login-fill-list"/>
</vbox>
<vbox id="login-fill-clickcapturer"/>
<vbox id="login-fill-details">
<textbox id="login-fill-username" readonly="true"/>
<textbox id="login-fill-password" type="password" disabled="true"/>
<hbox>
<button id="login-fill-use" label="Use in form"/>
</hbox>
</vbox>
</stack>
#ifdef E10S_TESTING_ONLY
<popupnotification id="enable-e10s-notification" hidden="true">

View File

@ -573,20 +573,6 @@ Sanitizer.prototype = {
let features = "chrome,all,dialog=no," + this.privateStateForNewWindow;
let newWindow = existingWindow.openDialog("chrome://browser/content/", "_blank",
features, defaultArgs);
#ifdef XP_MACOSX
function onFullScreen(e) {
newWindow.removeEventListener("fullscreen", onFullScreen);
let docEl = newWindow.document.documentElement;
let sizemode = docEl.getAttribute("sizemode");
if (!newWindow.fullScreen && sizemode == "fullscreen") {
docEl.setAttribute("sizemode", "normal");
e.preventDefault();
e.stopPropagation();
return false;
}
}
newWindow.addEventListener("fullscreen", onFullScreen);
#endif
// Window creation and destruction is asynchronous. We need to wait
// until all existing windows are fully closed, and the new window is
@ -600,9 +586,6 @@ Sanitizer.prototype = {
return;
Services.obs.removeObserver(onWindowOpened, "browser-delayed-startup-finished");
#ifdef XP_MACOSX
newWindow.removeEventListener("fullscreen", onFullScreen);
#endif
newWindowOpened = true;
// If we're the last thing to happen, invoke callback.
if (numWindowsClosing == 0) {

View File

@ -589,15 +589,31 @@ let DOMFullscreenHandler = {
_fullscreenDoc: null,
init: function() {
addMessageListener("DOMFullscreen:Entered", this);
addMessageListener("DOMFullscreen:Approved", this);
addMessageListener("DOMFullscreen:CleanUp", this);
addEventListener("MozDOMFullscreen:Entered", this);
addEventListener("MozDOMFullscreen:Request", this);
addEventListener("MozDOMFullscreen:NewOrigin", this);
addEventListener("MozDOMFullscreen:Exited", this);
},
get _windowUtils() {
return content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
},
receiveMessage: function(aMessage) {
switch(aMessage.name) {
case "DOMFullscreen:Entered": {
if (!this._windowUtils.handleFullscreenRequests() &&
!content.document.mozFullScreen) {
// If we don't actually have any pending fullscreen request
// to handle, neither we have been in fullscreen, tell the
// parent to just exit.
sendAsyncMessage("DOMFullscreen:Exited");
}
break;
}
case "DOMFullscreen:Approved": {
if (this._fullscreenDoc) {
Services.obs.notifyObservers(this._fullscreenDoc,
@ -607,9 +623,7 @@ let DOMFullscreenHandler = {
break;
}
case "DOMFullscreen:CleanUp": {
let utils = content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
utils.exitFullscreen();
this._windowUtils.exitFullscreen();
this._fullscreenDoc = null;
break;
}
@ -618,8 +632,8 @@ let DOMFullscreenHandler = {
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "MozDOMFullscreen:Entered": {
sendAsyncMessage("DOMFullscreen:Entered");
case "MozDOMFullscreen:Request": {
sendAsyncMessage("DOMFullscreen:Request");
break;
}
case "MozDOMFullscreen:NewOrigin": {

View File

@ -1498,6 +1498,9 @@
let wasActive = document.activeElement == aBrowser;
// Unmap the old outerWindowID.
this._outerWindowIDBrowserMap.delete(aBrowser.outerWindowID);
// Unhook our progress listener.
let tab = this.getTabForBrowser(aBrowser);
let index = tab._tPos;
@ -1529,6 +1532,9 @@
tab.removeAttribute("crashed");
} else {
aBrowser.messageManager.sendAsyncMessage("Browser:AppTab", { isAppTab: tab.pinned })
// Register the new outerWindowID.
this._outerWindowIDBrowserMap.set(aBrowser.outerWindowID, aBrowser);
}
if (wasActive)
@ -1794,13 +1800,6 @@
b = this._createBrowser({remote, uriIsAboutBlank});
}
// A remote browser doesn't initially have the outerWindowID
// set. Once a remote browser initializes, it sends the Browser:Init
// message, and we map the browser at that point.
if (!remote) {
this._outerWindowIDBrowserMap.set(b.outerWindowID, b);
}
let notificationbox = this.getNotificationBox(b);
var position = this.tabs.length - 1;
var uniqueId = this._generateUniquePanelID();
@ -1890,6 +1889,19 @@
// activeness in the tab switcher.
b.docShellIsActive = false;
// When addTab() is called with an URL that is not "about:blank" we
// set the "nodefaultsrc" attribute that prevents a frameLoader
// from being created as soon as the linked <browser> is inserted
// into the DOM. We thus have to register the new outerWindowID
// for non-remote browsers after we have called browser.loadURI().
//
// Note: Only do this of we still have a docShell. The TabOpen
// event was dispatched above and a gBrowser.removeTab() call from
// one of its listeners could cause us to fail here.
if (!remote && b.docShell) {
this._outerWindowIDBrowserMap.set(b.outerWindowID, b);
}
// Check if we're opening a tab related to the current tab and
// move it to after the current tab.
// aReferrerURI is null or undefined if the tab is opened from
@ -2503,9 +2515,29 @@
// Make sure to unregister any open URIs.
this._swapRegisteredOpenURIs(ourBrowser, aOtherBrowser);
// Unmap old outerWindowIDs.
this._outerWindowIDBrowserMap.delete(ourBrowser.outerWindowID);
let remoteBrowser = aOtherBrowser.ownerDocument.defaultView.gBrowser;
if (remoteBrowser) {
remoteBrowser._outerWindowIDBrowserMap.delete(aOtherBrowser.outerWindowID);
}
// Swap the docshells
ourBrowser.swapDocShells(aOtherBrowser);
if (ourBrowser.isRemoteBrowser) {
// Switch outerWindowIDs for remote browsers.
let ourOuterWindowID = ourBrowser._outerWindowID;
ourBrowser._outerWindowID = aOtherBrowser._outerWindowID;
aOtherBrowser._outerWindowID = ourOuterWindowID;
}
// Register new outerWindowIDs.
this._outerWindowIDBrowserMap.set(ourBrowser.outerWindowID, ourBrowser);
if (remoteBrowser) {
remoteBrowser._outerWindowIDBrowserMap.set(aOtherBrowser.outerWindowID, aOtherBrowser);
}
// Swap permanentKey properties.
let ourPermanentKey = ourBrowser.permanentKey;
ourBrowser.permanentKey = aOtherBrowser.permanentKey;

View File

@ -242,6 +242,7 @@ let gTests = [
is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
"webRTC-shareDevices", "panel using devices icon");
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -250,6 +251,7 @@ let gTests = [
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({audio: true, video: true});
yield closeStream();
}
@ -270,6 +272,7 @@ let gTests = [
is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
"webRTC-shareMicrophone", "panel using microphone icon");
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -277,6 +280,7 @@ let gTests = [
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Microphone", "expected microphone to be shared");
yield indicator;
yield checkSharingUI({audio: true});
yield closeStream();
}
@ -297,6 +301,7 @@ let gTests = [
is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
"webRTC-shareDevices", "panel using devices icon");
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -304,6 +309,7 @@ let gTests = [
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
yield indicator;
yield checkSharingUI({video: true});
yield closeStream();
}
@ -322,6 +328,7 @@ let gTests = [
// disable the camera
enableDevice("Camera", false);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -334,6 +341,7 @@ let gTests = [
is(getMediaCaptureState(), "Microphone",
"expected microphone to be shared");
yield indicator;
yield checkSharingUI({audio: true});
yield closeStream();
}
@ -352,6 +360,7 @@ let gTests = [
// disable the microphone
enableDevice("Microphone", false);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -364,6 +373,7 @@ let gTests = [
is(getMediaCaptureState(), "Camera",
"expected microphone to be shared");
yield indicator;
yield checkSharingUI({video: true});
yield closeStream();
}
@ -427,6 +437,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -435,6 +446,7 @@ let gTests = [
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({video: true, audio: true});
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
@ -469,6 +481,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -477,6 +490,7 @@ let gTests = [
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({video: true, audio: true});
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
@ -730,6 +744,7 @@ let gTests = [
Perms.add(uri, "microphone", Perms.ALLOW_ACTION);
Perms.add(uri, "camera", Perms.ALLOW_ACTION);
let indicator = promiseIndicatorWindow();
// Start sharing what's been requested.
yield promiseMessage("ok", () => {
content.wrappedJSObject.requestDevice(aRequestAudio, aRequestVideo);
@ -737,6 +752,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
expectObserverCalled("getUserMedia:response:allow");
expectObserverCalled("recording-device-events");
yield indicator;
yield checkSharingUI({video: aRequestVideo, audio: aRequestAudio});
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
@ -801,6 +817,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(false, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -808,6 +825,7 @@ let gTests = [
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Camera", "expected camera to be shared");
yield indicator;
yield checkSharingUI({video: true});
yield promisePopupNotificationShown("webRTC-sharingDevices", () => {

View File

@ -241,6 +241,7 @@ let gTests = [
is(PopupNotifications.panel.firstChild.getAttribute("popupid"),
"webRTC-shareDevices", "panel using devices icon");
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -249,6 +250,7 @@ let gTests = [
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({audio: true, video: true});
yield closeStream(global);
}
@ -265,6 +267,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -273,6 +276,7 @@ let gTests = [
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({video: true, audio: true});
yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices"));
@ -308,6 +312,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, true);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -316,6 +321,7 @@ let gTests = [
is(getMediaCaptureState(), "CameraAndMicrophone",
"expected camera and microphone to be shared");
yield indicator;
yield checkSharingUI({video: true, audio: true});
info("reloading the frame");
@ -370,6 +376,7 @@ let gTests = [
expectObserverCalled("getUserMedia:request");
checkDeviceSelectors(true, false);
let indicator = promiseIndicatorWindow();
yield promiseMessage("ok", () => {
PopupNotifications.panel.firstChild.button.click();
});
@ -377,6 +384,7 @@ let gTests = [
expectObserverCalled("recording-device-events");
is(getMediaCaptureState(), "Microphone", "microphone to be shared");
yield indicator;
yield checkSharingUI({video: false, audio: true});
expectNoObserverCalled();

View File

@ -122,7 +122,7 @@ let TRANSITIONS = [
// Loads the new page by calling browser.loadURI directly
function* loadURI(browser, uri) {
info("Calling browser.loadURI");
browser.loadURI(uri);
yield BrowserTestUtils.loadURI(browser, uri);
return true;
},

View File

@ -197,7 +197,7 @@ add_task(function* test_synchronous() {
info("2");
// Load another page
info("Loading about:robots");
gBrowser.selectedBrowser.loadURI("about:robots");
yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "about:robots");
is(gBrowser.selectedBrowser.isRemoteBrowser, false, "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");
@ -208,7 +208,7 @@ add_task(function* test_synchronous() {
info("3");
// Load the remote page again
info("Loading http://example.org/" + DUMMY_PATH);
gBrowser.loadURI("http://example.org/" + DUMMY_PATH);
yield BrowserTestUtils.loadURI(gBrowser.selectedBrowser, "http://example.org/" + DUMMY_PATH);
is(gBrowser.selectedBrowser.isRemoteBrowser, expectedRemote, "Remote attribute should be correct");
is(gBrowser.selectedBrowser.permanentKey, permanentKey, "browser.permanentKey is still the same");

View File

@ -26,7 +26,7 @@ function frame_script() {
*/
function prepareNonRemoteBrowser(aWindow, browser) {
browser.loadURI(NON_REMOTE_PAGE);
return waitForDocLoadComplete(browser);
return BrowserTestUtils.browserLoaded(browser);
}
registerCleanupFunction(() => {

View File

@ -633,10 +633,13 @@ function waitForNewTabEvent(aTabBrowser) {
* @resolves to the window
*/
function promiseWindow(url) {
info("waiting for a " + url + " window");
info("expecting a " + url + " window");
return new Promise(resolve => {
Services.obs.addObserver(function obs(win) {
win.QueryInterface(Ci.nsIDOMWindow);
win.addEventListener("load", function loadHandler() {
win.removeEventListener("load", loadHandler);
if (win.location.href !== url) {
info("ignoring a window with this url: " + win.location.href);
return;
@ -644,10 +647,19 @@ function promiseWindow(url) {
Services.obs.removeObserver(obs, "domwindowopened");
resolve(win);
});
}, "domwindowopened", false);
});
}
function promiseIndicatorWindow() {
// We don't show the indicator window on Mac.
if ("nsISystemStatusBar" in Ci)
return Promise.resolve();
return promiseWindow("chrome://browser/content/webrtcIndicator.xul");
}
function assertWebRTCIndicatorStatus(expected) {
let ui = Cu.import("resource:///modules/webrtcUI.jsm", {}).webrtcUI;
let expectedState = expected ? "visible" : "hidden";
@ -694,9 +706,6 @@ function assertWebRTCIndicatorStatus(expected) {
});
}
}
if (expected &&
!Services.wm.getMostRecentWindow("Browser:WebRTCGlobalIndicator"))
yield promiseWindow("chrome://browser/content/webrtcIndicator.xul");
let indicator = Services.wm.getEnumerator("Browser:WebRTCGlobalIndicator");
let hasWindow = indicator.hasMoreElements();
is(hasWindow, !!expected, "popup " + msg);

View File

@ -12,8 +12,6 @@
<!DOCTYPE page [
<!ENTITY % browserDTD SYSTEM "chrome://browser/locale/browser.dtd">
%browserDTD;
<!ENTITY % browserPocketDTD SYSTEM "chrome://browser/content/browser-pocket.dtd">
%browserPocketDTD;
<!ENTITY % textcontextDTD SYSTEM "chrome://global/locale/textcontext.dtd">
%textcontextDTD;
]>

View File

@ -75,12 +75,6 @@ browser.jar:
* content/browser/browser.css (content/browser.css)
* content/browser/browser.js (content/browser.js)
* content/browser/browser.xul (content/browser.xul)
content/browser/browser-pocket-en-US.properties (content/browser-pocket-en-US.properties)
content/browser/browser-pocket.dtd (content/browser-pocket.dtd)
content/browser/browser-pocket-de.properties (content/browser-pocket-de.properties)
content/browser/browser-pocket-es-ES.properties (content/browser-pocket-es-ES.properties)
content/browser/browser-pocket-ja.properties (content/browser-pocket-ja.properties)
content/browser/browser-pocket-ru.properties (content/browser-pocket-ru.properties)
* content/browser/browser-tabPreviews.xml (content/browser-tabPreviews.xml)
* content/browser/chatWindow.xul (content/chatWindow.xul)
content/browser/tab-content.js (content/tab-content.js)

View File

@ -13,37 +13,22 @@
<hbox id="identity-popup-container" align="top">
<image id="identity-popup-icon"/>
<vbox id="identity-popup-content-box">
<label id="identity-popup-brandName"
<label id="identity-popup-content-host"
class="identity-popup-description"
crop="end"/>
<label id="identity-popup-connection-secure"
class="identity-popup-label"
value="&brandFullName;"/>
<label id="identity-popup-chromeLabel"
class="identity-popup-label"/>
<label id="identity-popup-connectedToLabel"
value="&identity.connectionSecure;"/>
<label id="identity-popup-connection-not-secure"
class="identity-popup-label"
value="&identity.connectedTo;"/>
<label id="identity-popup-connectedToLabel2"
class="identity-popup-label"
value="&identity.unverifiedsite2;"/>
<description id="identity-popup-content-host"
class="identity-popup-description"/>
<label id="identity-popup-runByLabel"
class="identity-popup-label"
value="&identity.runBy;"/>
value="&identity.connectionNotSecure;"/>
<description id="identity-popup-content-owner"
class="identity-popup-description"/>
<description id="identity-popup-content-supplemental"
class="identity-popup-description"/>
<description id="identity-popup-content-verifier"
class="identity-popup-description"/>
<hbox id="identity-popup-encryption" flex="1">
<vbox>
<image id="identity-popup-encryption-icon"/>
</vbox>
<description id="identity-popup-encryption-label" flex="1"
class="identity-popup-description"/>
</hbox>
<vbox id="identity-popup-permissions">
<separator class="thin"/>
<label class="identity-popup-label header"
value="&identity.permissions;"/>
<vbox id="identity-popup-permission-list" class="indent"/>

View File

@ -1065,11 +1065,10 @@ if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) {
if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
let isEnabledForLocale = true;
let browserLocale;
if (Services.prefs.getBoolPref("browser.pocket.useLocaleList")) {
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
browserLocale = chromeRegistry.getSelectedLocale("browser");
let browserLocale = chromeRegistry.getSelectedLocale("browser");
let enabledLocales = [];
try {
enabledLocales = Services.prefs.getCharPref("browser.pocket.enabledLocales").split(' ');
@ -1080,32 +1079,12 @@ if (Services.prefs.getBoolPref("browser.pocket.enabled")) {
}
if (isEnabledForLocale) {
if (browserLocale == "ja-JP-mac")
browserLocale = "ja";
let url = "chrome://browser/content/browser-pocket-" + browserLocale + ".properties";
let strings = Services.strings.createBundle(url);
let label;
let tooltiptext;
try {
label = strings.GetStringFromName("pocket-button.label");
tooltiptext = strings.GetStringFromName("pocket-button.tooltiptext");
} catch (err) {
// GetStringFromName throws when the bundle doesn't exist. In that case,
// fall back to the en-US browser-pocket.properties.
url = "chrome://browser/content/browser-pocket-en-US.properties";
strings = Services.strings.createBundle(url);
label = strings.GetStringFromName("pocket-button.label");
tooltiptext = strings.GetStringFromName("pocket-button.tooltiptext");
}
let pocketButton = {
id: "pocket-button",
defaultArea: CustomizableUI.AREA_NAVBAR,
introducedInVersion: "pref",
type: "view",
viewId: "PanelUI-pocketView",
label: label,
tooltiptext: tooltiptext,
// Use forwarding functions here to avoid loading Pocket.jsm on startup:
onViewShowing: function() {
return Pocket.onPanelViewShowing.apply(this, arguments);

View File

@ -10,7 +10,9 @@ const kTimeoutInMS = 20000;
add_task(function() {
CustomizableUI.addWidgetToArea("zoom-controls", CustomizableUI.AREA_NAVBAR);
let tab1 = gBrowser.addTab("about:mozilla");
let tab2 = gBrowser.addTab("about:newtab");
yield BrowserTestUtils.browserLoaded(tab1.linkedBrowser);
let tab2 = gBrowser.addTab("about:robots");
yield BrowserTestUtils.browserLoaded(tab2.linkedBrowser);
gBrowser.selectedTab = tab1;
let zoomResetButton = document.getElementById("zoom-reset-button");
@ -30,7 +32,7 @@ add_task(function() {
let tabSelectPromise = promiseTabSelect();
gBrowser.selectedTab = tab2;
yield tabSelectPromise;
is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:newtab");
is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:robots");
gBrowser.selectedTab = tab1;
let zoomResetPromise = promiseObserverNotification("browser-fullZoom:zoomReset");
@ -52,6 +54,7 @@ add_task(function() {
return parseInt(zoomResetButton.label, 10) == 110;
});
is(parseInt(zoomResetButton.label, 10), 110, "Zoom is still 110% for about:mozilla");
FullZoom.reset();
});
function promiseObserverNotification(aObserver) {

View File

@ -134,7 +134,7 @@
background-color: #fff;
}
body[dir="rtl"] .contact > .details > .username > i.icon-google {
html[dir="rtl"] .contact > .details > .username > i.icon-google {
left: 1rem;
right: auto;
}
@ -193,7 +193,7 @@ body[dir="rtl"] .contact > .details > .username > i.icon-google {
left: auto;
}
body[dir="rtl"] .contact > .dropdown-menu {
html[dir="rtl"] .contact > .dropdown-menu {
right: auto;
left: 3em;
}
@ -211,7 +211,7 @@ body[dir="rtl"] .contact > .dropdown-menu {
margin-top: 3px;
}
body[dir="rtl"] .contact > .dropdown-menu > .dropdown-menu-item > .icon {
html[dir="rtl"] .contact > .dropdown-menu > .dropdown-menu-item > .icon {
background-position: center right;
}
@ -259,7 +259,7 @@ body[dir="rtl"] .contact > .dropdown-menu > .dropdown-menu-item > .icon {
word-wrap: break-word;
}
body[dir=rtl] .contacts-gravatar-promo > p {
html[dir="rtl"] .contacts-gravatar-promo > p {
margin-right: 0;
margin-left: 4px;
}
@ -279,7 +279,7 @@ body[dir=rtl] .contacts-gravatar-promo > p {
right: 8px;
}
body[dir=rtl] .contacts-gravatar-promo > .button-close {
html[dir="rtl"] .contacts-gravatar-promo > .button-close {
right: auto;
left: 8px;
}

View File

@ -265,7 +265,7 @@ body {
flex: 0 1 auto;
}
body[dir=rtl] .new-room-view > .context > .context-content > .context-preview {
html[dir="rtl"] .new-room-view > .context > .context-content > .context-preview {
float: left;
}
@ -567,7 +567,7 @@ body[dir=rtl] .new-room-view > .context > .context-content > .context-preview {
right: 4px;
}
body[dir=rtl] .generate-url-spinner {
html[dir="rtl"] .generate-url-spinner {
left: 4px;
right: auto;
}
@ -753,7 +753,7 @@ body[dir=rtl] .generate-url-spinner {
right: 14px;
}
body[dir="rtl"] .settings-menu .dropdown-menu {
html[dir="rtl"] .settings-menu .dropdown-menu {
/* This is specified separately rather than using -moz-margin-start etc, as
we need to override .dropdown-menu's values which can't use the gecko
specific extensions. */

View File

@ -163,7 +163,8 @@ loop.conversation = (function(mozL10n) {
dispatcher: dispatcher,
mozLoop: navigator.mozLoop}), document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.documentElement.setAttribute("lang", mozL10n.getLanguage());
document.documentElement.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({

View File

@ -163,7 +163,8 @@ loop.conversation = (function(mozL10n) {
dispatcher={dispatcher}
mozLoop={navigator.mozLoop} />, document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.documentElement.setAttribute("lang", mozL10n.getLanguage());
document.documentElement.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
dispatcher.dispatch(new sharedActions.GetWindowData({

View File

@ -565,13 +565,23 @@ loop.conversationViews = (function(mozL10n) {
var OngoingConversationView = React.createClass({displayName: "OngoingConversationView",
mixins: [
loop.store.StoreMixin("conversationStore"),
sharedMixins.MediaSetupMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// local
video: React.PropTypes.object,
audio: React.PropTypes.object
// local
audio: React.PropTypes.object,
remoteVideoEnabled: React.PropTypes.bool,
// This is used from the props rather than the state to make it easier for
// the ui-showcase.
mediaConnected: React.PropTypes.bool,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
remotePosterUrl: React.PropTypes.string
},
getDefaultProps: function() {
@ -581,6 +591,10 @@ loop.conversationViews = (function(mozL10n) {
};
},
getInitialState: function() {
return this.getStoreState();
},
componentDidMount: function() {
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
@ -588,9 +602,7 @@ loop.conversationViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
})
}));
},
@ -616,6 +628,18 @@ loop.conversationViews = (function(mozL10n) {
}));
},
shouldRenderRemoteVideo: function() {
if (this.props.mediaConnected) {
// If remote video is not enabled, we're muted, so we'll show an avatar
// instead.
return this.props.remoteVideoEnabled;
}
// We're not yet connected, but we don't want to show the avatar, and in
// the common case, we'll just transition to the video.
return true;
},
render: function() {
var localStreamClasses = React.addons.classSet({
local: true,
@ -628,11 +652,22 @@ loop.conversationViews = (function(mozL10n) {
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("div", {className: "video_inner remote focus-stream"},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
posterUrl: this.props.remotePosterUrl,
mediaType: "remote",
srcVideoObject: this.state.remoteSrcVideoObject})
)
),
React.createElement("div", {className: localStreamClasses})
React.createElement("div", {className: localStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.props.video.enabled,
posterUrl: this.props.localPosterUrl,
mediaType: "local",
srcVideoObject: this.state.localSrcVideoObject})
)
),
React.createElement(loop.shared.views.ConversationToolbar, {
dispatcher: this.props.dispatcher,
video: this.props.video,
audio: this.props.audio,
publishStream: this.publishStream,
@ -742,7 +777,10 @@ loop.conversationViews = (function(mozL10n) {
return (React.createElement(OngoingConversationView, {
dispatcher: this.props.dispatcher,
video: {enabled: !this.state.videoMuted},
audio: {enabled: !this.state.audioMuted}}
audio: {enabled: !this.state.audioMuted},
remoteVideoEnabled: this.state.remoteVideoEnabled,
mediaConnected: this.state.mediaConnected,
remoteSrcVideoObject: this.state.remoteSrcVideoObject}
)
);
}

View File

@ -565,13 +565,23 @@ loop.conversationViews = (function(mozL10n) {
var OngoingConversationView = React.createClass({
mixins: [
loop.store.StoreMixin("conversationStore"),
sharedMixins.MediaSetupMixin
],
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
// local
video: React.PropTypes.object,
audio: React.PropTypes.object
// local
audio: React.PropTypes.object,
remoteVideoEnabled: React.PropTypes.bool,
// This is used from the props rather than the state to make it easier for
// the ui-showcase.
mediaConnected: React.PropTypes.bool,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
remotePosterUrl: React.PropTypes.string
},
getDefaultProps: function() {
@ -581,6 +591,10 @@ loop.conversationViews = (function(mozL10n) {
};
},
getInitialState: function() {
return this.getStoreState();
},
componentDidMount: function() {
// The SDK needs to know about the configuration and the elements to use
// for display. So the best way seems to pass the information here - ideally
@ -588,9 +602,7 @@ loop.conversationViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: this.props.video.enabled
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
})
}));
},
@ -616,6 +628,18 @@ loop.conversationViews = (function(mozL10n) {
}));
},
shouldRenderRemoteVideo: function() {
if (this.props.mediaConnected) {
// If remote video is not enabled, we're muted, so we'll show an avatar
// instead.
return this.props.remoteVideoEnabled;
}
// We're not yet connected, but we don't want to show the avatar, and in
// the common case, we'll just transition to the video.
return true;
},
render: function() {
var localStreamClasses = React.addons.classSet({
local: true,
@ -628,11 +652,22 @@ loop.conversationViews = (function(mozL10n) {
<div className="conversation">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote focus-stream"></div>
<div className="video_inner remote focus-stream">
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
posterUrl={this.props.remotePosterUrl}
mediaType="remote"
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
</div>
<div className={localStreamClasses}>
<sharedViews.MediaView displayAvatar={!this.props.video.enabled}
posterUrl={this.props.localPosterUrl}
mediaType="local"
srcVideoObject={this.state.localSrcVideoObject} />
</div>
<div className={localStreamClasses}></div>
</div>
<loop.shared.views.ConversationToolbar
dispatcher={this.props.dispatcher}
video={this.props.video}
audio={this.props.audio}
publishStream={this.publishStream}
@ -743,6 +778,9 @@ loop.conversationViews = (function(mozL10n) {
dispatcher={this.props.dispatcher}
video={{enabled: !this.state.videoMuted}}
audio={{enabled: !this.state.audioMuted}}
remoteVideoEnabled={this.state.remoteVideoEnabled}
mediaConnected={this.state.mediaConnected}
remoteSrcVideoObject={this.state.remoteSrcVideoObject}
/>
);
}

View File

@ -1002,7 +1002,8 @@ loop.panel = (function(_, mozL10n) {
mozLoop: navigator.mozLoop,
dispatcher: dispatcher}), document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.documentElement.setAttribute("lang", mozL10n.getLanguage());
document.documentElement.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
// Notify the window that we've finished initalization and initial layout

View File

@ -1002,7 +1002,8 @@ loop.panel = (function(_, mozL10n) {
mozLoop={navigator.mozLoop}
dispatcher={dispatcher} />, document.querySelector("#main"));
document.body.setAttribute("dir", mozL10n.getDirection());
document.documentElement.setAttribute("lang", mozL10n.getLanguage());
document.documentElement.setAttribute("dir", mozL10n.getDirection());
document.body.setAttribute("platform", loop.shared.utils.getPlatform());
// Notify the window that we've finished initalization and initial layout

View File

@ -513,7 +513,11 @@ loop.store = loop.store || {};
// When no properties have been set on the roomData object, there's nothing
// to save.
if (!Object.getOwnPropertyNames(roomData).length) {
// Ensure async actions so that we get separate setStoreState events
// that React components won't miss.
setTimeout(function() {
this.dispatchAction(new sharedActions.UpdateRoomContextDone());
}.bind(this), 0);
return;
}

View File

@ -338,6 +338,13 @@ loop.roomViews = (function(mozL10n) {
}
}
// Make sure we do not show the edit-mode when we just successfully saved
// context.
if (this.props.savingContext && nextProps.savingContext !== this.props.savingContext &&
!nextProps.error && this.state.editMode) {
newState.editMode = false;
}
if (Object.getOwnPropertyNames(newState).length) {
this.setState(newState);
}
@ -528,7 +535,7 @@ loop.roomViews = (function(mozL10n) {
React.createElement("button", {className: "btn btn-info",
disabled: this.props.savingContext,
onClick: this.handleFormSubmit},
mozL10n.get("context_save_label")
mozL10n.get("context_save_label2")
),
React.createElement("button", {className: "room-context-btn-close",
onClick: this.handleCloseClick,
@ -579,7 +586,10 @@ loop.roomViews = (function(mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired
mozLoop: React.PropTypes.object.isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
remotePosterUrl: React.PropTypes.string
},
componentWillUpdate: function(nextProps, nextState) {
@ -591,10 +601,7 @@ loop.roomViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
})
}));
}
},
@ -635,6 +642,40 @@ loop.roomViews = (function(mozL10n) {
);
},
/**
* Works out if remote video should be rended or not, depending on the
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
return true;
}
if (this.state.mediaConnected) {
// since the remoteVideo hasn't yet been enabled, if the
// media is connected, then we should be displaying an avatar.
return false;
}
return true;
case ROOM_STATES.SESSION_CONNECTED:
case ROOM_STATES.JOINED:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
default:
console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;
}
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -674,6 +715,7 @@ loop.roomViews = (function(mozL10n) {
);
}
default: {
return (
React.createElement("div", {className: "room-conversation-wrapper"},
React.createElement(sharedViews.TextChatView, {dispatcher: this.props.dispatcher}),
@ -690,10 +732,19 @@ loop.roomViews = (function(mozL10n) {
React.createElement("div", {className: "conversation room-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("div", {className: "video_inner remote focus-stream"},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
posterUrl: this.props.remotePosterUrl,
mediaType: "remote",
srcVideoObject: this.state.remoteSrcVideoObject})
)
),
React.createElement("div", {className: localStreamClasses}),
React.createElement("div", {className: "screen hide"})
React.createElement("div", {className: localStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted,
posterUrl: this.props.localPosterUrl,
mediaType: "local",
srcVideoObject: this.state.localSrcVideoObject})
)
),
React.createElement(sharedViews.ConversationToolbar, {
dispatcher: this.props.dispatcher,

View File

@ -338,6 +338,13 @@ loop.roomViews = (function(mozL10n) {
}
}
// Make sure we do not show the edit-mode when we just successfully saved
// context.
if (this.props.savingContext && nextProps.savingContext !== this.props.savingContext &&
!nextProps.error && this.state.editMode) {
newState.editMode = false;
}
if (Object.getOwnPropertyNames(newState).length) {
this.setState(newState);
}
@ -528,7 +535,7 @@ loop.roomViews = (function(mozL10n) {
<button className="btn btn-info"
disabled={this.props.savingContext}
onClick={this.handleFormSubmit}>
{mozL10n.get("context_save_label")}
{mozL10n.get("context_save_label2")}
</button>
<button className="room-context-btn-close"
onClick={this.handleCloseClick}
@ -579,7 +586,10 @@ loop.roomViews = (function(mozL10n) {
propTypes: {
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
mozLoop: React.PropTypes.object.isRequired
mozLoop: React.PropTypes.object.isRequired,
// The poster URLs are for UI-showcase testing and development.
localPosterUrl: React.PropTypes.string,
remotePosterUrl: React.PropTypes.string
},
componentWillUpdate: function(nextProps, nextState) {
@ -591,10 +601,7 @@ loop.roomViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({
publishVideo: !this.state.videoMuted
}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen"),
getRemoteElementFunc: this._getElement.bind(this, ".remote")
})
}));
}
},
@ -635,6 +642,40 @@ loop.roomViews = (function(mozL10n) {
);
},
/**
* Works out if remote video should be rended or not, depending on the
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
return true;
}
if (this.state.mediaConnected) {
// since the remoteVideo hasn't yet been enabled, if the
// media is connected, then we should be displaying an avatar.
return false;
}
return true;
case ROOM_STATES.SESSION_CONNECTED:
case ROOM_STATES.JOINED:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
default:
console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;
}
},
render: function() {
if (this.state.roomName) {
this.setTitle(this.state.roomName);
@ -674,6 +715,7 @@ loop.roomViews = (function(mozL10n) {
);
}
default: {
return (
<div className="room-conversation-wrapper">
<sharedViews.TextChatView dispatcher={this.props.dispatcher} />
@ -690,10 +732,19 @@ loop.roomViews = (function(mozL10n) {
<div className="conversation room-conversation">
<div className="media nested">
<div className="video_wrapper remote_wrapper">
<div className="video_inner remote focus-stream"></div>
<div className="video_inner remote focus-stream">
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
posterUrl={this.props.remotePosterUrl}
mediaType="remote"
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
</div>
<div className={localStreamClasses}>
<sharedViews.MediaView displayAvatar={this.state.videoMuted}
posterUrl={this.props.localPosterUrl}
mediaType="local"
srcVideoObject={this.state.localSrcVideoObject} />
</div>
<div className={localStreamClasses}></div>
<div className="screen hide"></div>
</div>
<sharedViews.ConversationToolbar
dispatcher={this.props.dispatcher}

View File

@ -38,8 +38,9 @@
// translate a string
function translateString(key, args, fallback) {
if (args && args.num) {
var num = args && args.num;
var num;
if (args && ("num" in args)) {
num = args.num;
}
var data = getL10nData(key, num);
if (!data && fallback)

View File

@ -426,7 +426,7 @@ p {
border-radius: 2px;
}
body[dir=rtl] .dropdown-menu {
html[dir="rtl"] .dropdown-menu {
left: auto;
right: 0;
}
@ -481,7 +481,7 @@ body[dir=rtl] .dropdown-menu {
background-size: 1em 1em;
}
body[dir="rtl"] .checkbox {
html[dir="rtl"] .checkbox {
float: right;
}

View File

@ -254,6 +254,12 @@
left: 0px;
}
.fx-embedded .no-video {
background: black none repeat scroll 0% 0%;
height: 100%;
width: 100%;
}
.standalone .local-stream,
.standalone .remote-inset-stream {
/* required to have it superimposed to the control toolbar */
@ -512,11 +518,6 @@
width: 30%;
height: 28%;
max-height: 105px;
box-shadow: 0px 2px 4px rgba(0,0,0,.5);
}
.fx-embedded .room-conversation .local-stream {
box-shadow: none;
}
.fx-embedded .local-stream.room-preview {
@ -540,73 +541,32 @@
right: 0;
}
/*
* XXX this approach is fragile because it makes assumptions
* about the generated OT markup, any change will break it
*/
/*
* For any audio-only streams, we want to display our own background
*/
.OT_audio-only .OT_widget-container .OT_video-poster {
.avatar {
background-image: url("../img/audio-call-avatar.svg");
background-repeat: no-repeat;
background-color: #4BA6E7;
background-size: contain;
background-position: center;
}
/*
* Audio-only. For local streams, cancel out the SDK's opacity of 0.25.
* For remote streams we leave them shaded, as otherwise its too bright.
/*
* Expand to fill the available space, since there is no video any
* intrinsic width. XXX should really change to an <img> for clarity
*/
.local-stream-audio .OT_publisher .OT_video-poster {
opacity: 1
height: 100%;
width: 100%;
}
/*
* In audio-only mode, don't display the video element, doing so interferes
* with the background opacity of the video-poster element.
*/
.OT_audio-only .OT_widget-container .OT_video-element {
display: none;
.local .avatar {
position: absolute;
z-index: 1;
}
/*
* Ensure that the publisher (i.e. local) video is never cropped, so that it's
* not possible for someone to be presented with a picture that displays
* (for example) a person from the neck up, even though the camera is capturing
* and transmitting a picture of that person from the waist up.
*
* The !importants are necessary to override the SDK attempts to avoid
* letterboxing entirely.
*
* If we could easily use test video streams with the SDK (eg if initPublisher
* supported something like a "testMediaToStreamURI" parameter that it would
* use to source the stream rather than the output of gUM, it wouldn't be too
* hard to generate a video with a 1 pixel border at the edges that one could
* at least visually see wasn't being cropped.
*
* Another less ugly possibility would be to work with Ted Mielczarek to use
* the fake camera drivers he has for Linux.
*/
.room-conversation .OT_publisher .OT_widget-container {
height: 100% !important;
width: 100% !important;
top: 0 !important;
left: 0 !important;
background-color: transparent; /* avoid visually obvious letterboxing */
}
.room-conversation .OT_publisher .OT_widget-container video {
background-color: transparent; /* avoid visually obvious letterboxing */
}
.fx-embedded .room-conversation .room-preview .OT_publisher .OT_widget-container,
.fx-embedded .room-conversation .room-preview .OT_publisher .OT_widget-container video {
/* Desktop conversation window room preview local stream actually wants
a black background */
background-color: #000;
.remote .avatar {
/* make visually distinct from local avatar */
opacity: 0.25;
}
.fx-embedded .media.nested {
@ -712,7 +672,8 @@ html, .fx-embedded, #main,
margin: auto;
}
@media screen and (min-width:640px) {
/* We use 641px rather than 640, as min-width and max-width are inclusive */
@media screen and (min-width:641px) {
.standalone .conversation-toolbar {
position: absolute;
bottom: 0;
@ -766,11 +727,6 @@ html, .fx-embedded, #main,
height: 90%;
}
.standalone .OT_subscriber {
height: 100%;
width: auto;
}
.standalone .media.nested {
min-height: 500px;
}
@ -798,7 +754,7 @@ html, .fx-embedded, #main,
.standalone .video_wrapper.remote_wrapper {
/* Because of OT markup we need to set a high flex value
* Flex rule assures remote and local streams stack on top of eachother
* Flex rule assures remote and local streams stack on top of each other
* Computed width is not 100% unless the `width` rule */
flex: 2;
width: 100%;
@ -923,7 +879,7 @@ html, .fx-embedded, #main,
text-decoration: underline;
}
body[dir="rtl"] .room-invitation-addcontext {
html[dir="rtl"] .room-invitation-addcontext {
padding-left: 0;
padding-right: 1.5em;
background-position: right top;
@ -1152,13 +1108,13 @@ body[platform="win"] .share-service-dropdown.overflow > .dropdown-menu-item {
background-image: url("../img/icons-10x10.svg#close-active");
}
body[dir=rtl] .room-context-btn-close,
body[dir=rtl] .room-context-btn-edit {
html[dir="rtl"] .room-context-btn-close,
html[dir="rtl"] .room-context-btn-edit {
right: auto;
left: 8px;
}
body[dir=rtl] .room-context-btn-edit {
html[dir="rtl"] .room-context-btn-edit {
left: 20px;
}
@ -1278,7 +1234,7 @@ body[dir=rtl] .room-context-btn-edit {
.standalone .room-conversation .video_wrapper.remote_wrapper {
background-color: #4e4e4e;
width: 75%;
width: calc(75% - 10px); /* Take the left margin into account. */
}
.standalone .room-conversation .conversation-toolbar {
@ -1401,7 +1357,7 @@ body[dir=rtl] .room-context-btn-edit {
@media screen and (max-height:160px) {
/* disable the self view */
.standalone .OT_publisher {
.standalone .local-video {
display: none;
}
@ -1412,3 +1368,36 @@ body[dir=rtl] .room-context-btn-edit {
top: 90px;
}
}
.remote-video {
/* Since there is grey stuff behind us, avoid obvious letterboxing, only do
* this on remote video as local video has transparent background.
*/
background-color: black;
}
.standalone .screen.focus-stream {
/* Since there is grey stuff behind us, avoid obvious letterboxing */
background-color: black;
}
.local-video {
width: 100%;
height: 100%;
/* Transform is to make the local video act like a mirror, as is the
convention in video conferencing systems. */
transform: scale(-1, 1);
transform-origin: 50% 50% 0;
}
.remote-video {
width: 100%;
height: 100%;
display: block;
position: absolute;
}
.screen-share-video {
width: 100%;
height: 100%;
}

View File

@ -193,14 +193,7 @@ loop.shared.actions = (function() {
*/
SetupStreamElements: Action.define("setupStreamElements", {
// The configuration for the publisher/subscribe options
publisherConfig: Object,
// The local stream element
getLocalElementFunc: Function,
// The screen share element; optional until all conversation
// types support it.
// getScreenShareElementFunc: Function,
// The remote stream element
getRemoteElementFunc: Function
publisherConfig: Object
}),
/**
@ -225,6 +218,42 @@ loop.shared.actions = (function() {
dimensions: Object
}),
/**
* Video has been enabled from the remote sender.
*
* XXX somewhat tangled up with remote video muting semantics; see bug
* 1171969
*
* @note if/when we want to untangle this, we'll may want to include the
* reason provided by the SDK and documented hereI:
* https://tokbox.com/opentok/libraries/client/js/reference/VideoEnabledChangedEvent.html
*/
RemoteVideoEnabled: Action.define("remoteVideoEnabled", {
/* The SDK video object that the views will be copying the remote
stream from. */
srcVideoObject: Object
}),
/**
* Video has been disabled by the remote sender.
*
* @see RemoteVideoEnabled
*/
RemoteVideoDisabled: Action.define("remoteVideoDisabled", {
}),
/**
* Video from the local camera has been enabled.
*
* XXX we should implement a LocalVideoDisabled action to cleanly prevent
* leakage; see bug 1171978 for details
*/
LocalVideoEnabled: Action.define("localVideoEnabled", {
/* The SDK video object that the views will be copying the remote
stream from. */
srcVideoObject: Object
}),
/**
* Used to mute or unmute a stream
*/
@ -250,7 +279,7 @@ loop.shared.actions = (function() {
}),
/**
* Used to notifiy that screen sharing is active or not.
* Used to notify that screen sharing is active or not.
*/
ScreenSharingState: Action.define("screenSharingState", {
// One of loop.shared.utils.SCREEN_SHARE_STATES.
@ -259,9 +288,13 @@ loop.shared.actions = (function() {
/**
* Used to notify that a shared screen is being received (or not).
*
* XXX this is going to need to be split into two actions so when
* can display a spinner when the screen share is pending (bug 1171933)
*/
ReceivingScreenShare: Action.define("receivingScreenShare", {
receiving: Boolean
// srcVideoObject: Object (only present if receiving is true)
}),
/**

View File

@ -77,10 +77,15 @@ loop.store.ActiveRoomStore = (function() {
*/
_statesToResetOnLeave: [
"audioMuted",
"localSrcVideoObject",
"localVideoDimensions",
"mediaConnected",
"receivingScreenShare",
"remoteSrcVideoObject",
"remoteVideoDimensions",
"remoteVideoEnabled",
"screenSharingState",
"screenShareVideoObject",
"videoMuted"
],
@ -95,6 +100,7 @@ loop.store.ActiveRoomStore = (function() {
roomState: ROOM_STATES.INIT,
audioMuted: false,
videoMuted: false,
remoteVideoEnabled: false,
failureReason: undefined,
// Tracks if the room has been used during this
// session. 'Used' means at least one call has been placed
@ -115,7 +121,10 @@ loop.store.ActiveRoomStore = (function() {
roomInfoFailure: null,
// The name of the room.
roomName: null,
socialShareProviders: null
// Social API state.
socialShareProviders: null,
// True if media has been connected both-ways.
mediaConnected: false
};
},
@ -169,11 +178,15 @@ loop.store.ActiveRoomStore = (function() {
"windowUnload",
"leaveRoom",
"feedbackComplete",
"localVideoEnabled",
"remoteVideoEnabled",
"remoteVideoDisabled",
"videoDimensionsChanged",
"startScreenShare",
"endScreenShare",
"updateSocialShareInfo",
"connectionStatus"
"connectionStatus",
"mediaConnected"
]);
},
@ -550,6 +563,41 @@ loop.store.ActiveRoomStore = (function() {
this.setStoreState(muteState);
},
/**
* Records the local video object for the room.
*
* @param {sharedActions.LocalVideoEnabled} actionData
*/
localVideoEnabled: function(actionData) {
this.setStoreState({localSrcVideoObject: actionData.srcVideoObject});
},
/**
* Records the remote video object for the room.
*
* @param {sharedActions.RemoteVideoEnabled} actionData
*/
remoteVideoEnabled: function(actionData) {
this.setStoreState({
remoteVideoEnabled: true,
remoteSrcVideoObject: actionData.srcVideoObject
});
},
/**
* Records when remote video is disabled (e.g. due to mute).
*/
remoteVideoDisabled: function() {
this.setStoreState({remoteVideoEnabled: false});
},
/**
* Records when the remote media has been connected.
*/
mediaConnected: function() {
this.setStoreState({mediaConnected: true});
},
/**
* Used to note the current screensharing state.
*/
@ -563,6 +611,9 @@ loop.store.ActiveRoomStore = (function() {
/**
* Used to note the current state of receiving screenshare data.
*
* XXX this is going to need to be split into two actions so when
* can display a spinner when the screen share is pending (bug 1171933)
*/
receivingScreenShare: function(actionData) {
if (!actionData.receiving &&
@ -573,10 +624,15 @@ loop.store.ActiveRoomStore = (function() {
delete newDimensions.screen;
this.setStoreState({
receivingScreenShare: actionData.receiving,
remoteVideoDimensions: newDimensions
remoteVideoDimensions: newDimensions,
screenShareVideoObject: null
});
} else {
this.setStoreState({receivingScreenShare: actionData.receiving});
this.setStoreState({
receivingScreenShare: actionData.receiving,
screenShareVideoObject: actionData.srcVideoObject ?
actionData.srcVideoObject : null
});
}
},
@ -676,7 +732,10 @@ loop.store.ActiveRoomStore = (function() {
* one participantleaves.
*/
remotePeerDisconnected: function() {
this.setStoreState({roomState: ROOM_STATES.SESSION_CONNECTED});
this.setStoreState({
roomState: ROOM_STATES.SESSION_CONNECTED,
remoteSrcVideoObject: null
});
},
/**

View File

@ -93,6 +93,8 @@ loop.store = loop.store || {};
callId: undefined,
// The caller id of the contacting side
callerId: undefined,
// True if media has been connected both-ways.
mediaConnected: false,
// The connection progress url to connect the websocket
progressURL: undefined,
// The websocket token that allows connection to the progress url
@ -103,10 +105,11 @@ loop.store = loop.store || {};
sessionId: undefined,
// SDK session token
sessionToken: undefined,
// If the audio is muted
// If the local audio is muted
audioMuted: false,
// If the video is muted
videoMuted: false
// If the local video is muted
videoMuted: false,
remoteVideoEnabled: false
};
},
@ -232,6 +235,9 @@ loop.store = loop.store || {};
"mediaConnected",
"setMute",
"fetchRoomEmailLink",
"localVideoEnabled",
"remoteVideoDisabled",
"remoteVideoEnabled",
"windowUnload"
]);
@ -408,6 +414,7 @@ loop.store = loop.store || {};
*/
mediaConnected: function() {
this._websocket.mediaUp();
this.setStoreState({mediaConnected: true});
},
/**
@ -440,6 +447,44 @@ loop.store = loop.store || {};
}.bind(this));
},
/**
* Handles when the remote stream has been enabled and is supplied.
*
* @param {sharedActions.RemoteVideoEnabled} actionData
*/
remoteVideoEnabled: function(actionData) {
this.setStoreState({
remoteVideoEnabled: true,
remoteSrcVideoObject: actionData.srcVideoObject
});
},
/**
* Handles when the remote stream has been disabled, e.g. due to video mute.
*
* @param {sharedActions.RemoteVideoDisabled} actionData
*/
remoteVideoDisabled: function(actionData) {
this.setStoreState({
remoteVideoEnabled: false,
remoteSrcVideoObject: undefined});
},
/**
* Handles when the local stream is supplied.
*
* XXX should write a localVideoDisabled action in otSdkDriver.js to
* positively ensure proper cleanup (handled by window teardown currently)
* (see bug 1171978)
*
* @param {sharedActions.LocalVideoEnabled} actionData
*/
localVideoEnabled: function(actionData) {
this.setStoreState({
localSrcVideoObject: actionData.srcVideoObject
});
},
/**
* Called when the window is unloaded, either by code, or by the user
* explicitly closing it. Expected to do any necessary housekeeping, such

View File

@ -104,9 +104,6 @@ loop.OTSdkDriver = (function() {
* with the action. See action.js.
*/
setupStreamElements: function(actionData) {
this.getLocalElement = actionData.getLocalElementFunc;
this.getScreenShareElementFunc = actionData.getScreenShareElementFunc;
this.getRemoteElement = actionData.getRemoteElementFunc;
this.publisherConfig = actionData.publisherConfig;
this.sdk.on("exception", this._onOTException.bind(this));
@ -122,8 +119,13 @@ loop.OTSdkDriver = (function() {
* XXX This can be simplified when bug 1138851 is actioned.
*/
_publishLocalStreams: function() {
this.publisher = this.sdk.initPublisher(this.getLocalElement(),
// 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");
this.publisher = this.sdk.initPublisher(this._mockPublisherEl,
_.extend(this._getDataChannelSettings, this._getCopyPublisherConfig));
this.publisher.on("streamCreated", this._onLocalStreamCreated.bind(this));
this.publisher.on("streamDestroyed", this._onLocalStreamDestroyed.bind(this));
this.publisher.on("accessAllowed", this._onPublishComplete.bind(this));
@ -182,7 +184,9 @@ loop.OTSdkDriver = (function() {
var config = _.extend(this._getCopyPublisherConfig, options);
this.screenshare = this.sdk.initPublisher(this.getScreenShareElementFunc(),
this._mockScreenSharePreviewEl = document.createElement("div");
this.screenshare = this.sdk.initPublisher(this._mockScreenSharePreviewEl,
config);
this.screenshare.on("accessAllowed", this._onScreenShareGranted.bind(this));
this.screenshare.on("accessDenied", this._onScreenShareDenied.bind(this));
@ -209,7 +213,7 @@ loop.OTSdkDriver = (function() {
* Ends an active screenshare session. Return `true` when an active screen-
* sharing session was ended or `false` when no session is active.
*
* @type {Boolean}
* @returns {Boolean}
*/
endScreenShare: function() {
if (!this.screenshare) {
@ -222,6 +226,7 @@ loop.OTSdkDriver = (function() {
this.screenshare.off("accessAllowed accessDenied streamCreated");
this.screenshare.destroy();
delete this.screenshare;
delete this._mockScreenSharePreviewEl;
this._noteSharingState(this._windowId ? "browser" : "window", false);
delete this._windowId;
return true;
@ -289,6 +294,7 @@ loop.OTSdkDriver = (function() {
delete this._publisherReady;
delete this._publishedLocalStream;
delete this._subscribedRemoteStream;
delete this._mockPublisherEl;
this.connections = {};
this._setTwoWayMediaStartTime(this.CONNECTION_START_TIME_UNINITIALIZED);
},
@ -499,19 +505,23 @@ loop.OTSdkDriver = (function() {
* https://tokbox.com/opentok/libraries/client/js/reference/Stream.html
*/
_handleRemoteScreenShareCreated: function(stream) {
if (!this.getScreenShareElementFunc) {
return;
}
// Let the stores know first so they can update the display.
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: true
}));
// XXX We do want to do this - we want them to start re-arranging the
// display so that we can a) indicate connecting, b) be ready for
// when we get the stream. However, we're currently limited by the fact
// the view calculations require the remote (aka screen share) element to
// be present and laid out. Hence, we need to drop this for the time being,
// and let the client know via _onScreenShareSubscribeCompleted.
// Bug 1171933 is going to look at fixing this.
// this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
// receiving: true
// }));
var remoteElement = this.getScreenShareElementFunc();
this.session.subscribe(stream,
remoteElement, this._getCopyPublisherConfig);
// There's no audio for screen shares so we don't need to worry about mute.
this._mockScreenShareEl = document.createElement("div");
this.session.subscribe(stream, this._mockScreenShareEl,
this._getCopyPublisherConfig,
this._onScreenShareSubscribeCompleted.bind(this));
},
/**
@ -536,17 +546,88 @@ loop.OTSdkDriver = (function() {
return;
}
var remoteElement = this.getRemoteElement();
// Setting up the subscribe might want to be before the VideoDimensionsChange
// dispatch. If so, we might also want to consider moving the dispatch to
// _onSubscribeCompleted. However, this seems to work fine at the moment,
// so we haven't felt the need to move it.
// XXX This mock element currently handles playing audio for the session.
// We might want to consider making the react tree responsible for playing
// the audio, so that the incoming audio could be disable/tracked easly from
// the UI (bug 1171896).
this._mockSubscribeEl = document.createElement("div");
this.subscriber = this.session.subscribe(event.stream,
remoteElement, this._getCopyPublisherConfig,
this._onRemoteSessionSubscribed.bind(this, event.stream.connection));
this._mockSubscribeEl, this._getCopyPublisherConfig,
this._onSubscribeCompleted.bind(this));
},
/**
* This method is passed as the "completionHandler" parameter to the SDK's
* Session.subscribe.
*
* @param err {(null|Error)} - null on success, an Error object otherwise
* @param sdkSubscriberObject {OT.Subscriber} - undocumented; returned on success
* @param subscriberVideo {HTMLVideoElement} - used for unit testing
*/
_onSubscribeCompleted: function(err, sdkSubscriberObject, subscriberVideo) {
// XXX test for and handle errors better (bug 1172140)
if (err) {
console.log("subscribe error:", err);
return;
}
var sdkSubscriberVideo = subscriberVideo ? subscriberVideo :
this._mockSubscribeEl.querySelector("video");
if (!sdkSubscriberVideo) {
console.error("sdkSubscriberVideo unexpectedly falsy!");
}
sdkSubscriberObject.on("videoEnabled", this._onVideoEnabled.bind(this));
sdkSubscriberObject.on("videoDisabled", this._onVideoDisabled.bind(this));
// XXX for some reason, the SDK deliberately suppresses sending the
// videoEnabled event after subscribe, in spite of docs claiming
// otherwise, so we do it ourselves.
if (sdkSubscriberObject.stream.hasVideo) {
this.dispatcher.dispatch(new sharedActions.RemoteVideoEnabled({
srcVideoObject: sdkSubscriberVideo}));
}
this._subscribedRemoteStream = true;
if (this._checkAllStreamsConnected()) {
this._setTwoWayMediaStartTime(performance.now());
this.dispatcher.dispatch(new sharedActions.MediaConnected());
}
this._setupDataChannelIfNeeded(sdkSubscriberObject.stream.connection);
},
/**
* This method is passed as the "completionHandler" parameter to the SDK's
* Session.subscribe.
*
* @param err {(null|Error)} - null on success, an Error object otherwise
* @param sdkSubscriberObject {OT.Subscriber} - undocumented; returned on success
* @param subscriberVideo {HTMLVideoElement} - used for unit testing
*/
_onScreenShareSubscribeCompleted: function(err, sdkSubscriberObject, subscriberVideo) {
// XXX test for and handle errors better
if (err) {
console.log("subscribe error:", err);
return;
}
var sdkSubscriberVideo = subscriberVideo ? subscriberVideo :
this._mockScreenShareEl.querySelector("video");
// XXX no idea why this is necessary in addition to the dispatch in
// _handleRemoteScreenShareCreated. Maybe these should be separate
// actions. But even so, this shouldn't be necessary....
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: true, srcVideoObject: sdkSubscriberVideo
}));
},
/**
@ -554,16 +635,11 @@ loop.OTSdkDriver = (function() {
* channel set-up routines. A data channel cannot be requested before this
* time as the peer connection is not set up.
*
* @param {OT.Connection} connection The OT connection class object.
* @param {OT.Error} err Indicates if there's been an error in
* completing the subscribe.
* @param {OT.Connection} connection The OT connection class object.paul
* sched
*
*/
_onRemoteSessionSubscribed: function(connection, err) {
if (err) {
console.error(err);
return;
}
_setupDataChannelIfNeeded: function(connection) {
if (this._useDataChannels) {
this.session.signal({
type: "readyForDataChannel",
@ -670,6 +746,12 @@ loop.OTSdkDriver = (function() {
this._notifyMetricsEvent("Publisher.streamCreated");
if (event.stream[STREAM_PROPERTIES.HAS_VIDEO]) {
var sdkLocalVideo = this._mockPublisherEl.querySelector("video");
this.dispatcher.dispatch(new sharedActions.LocalVideoEnabled(
{srcVideoObject: sdkLocalVideo}));
this.dispatcher.dispatch(new sharedActions.VideoDimensionsChanged({
isLocal: true,
videoType: event.stream.videoType,
@ -739,6 +821,7 @@ loop.OTSdkDriver = (function() {
this._notifyMetricsEvent("Session.streamDestroyed");
if (event.stream.videoType !== "screen") {
delete this._mockSubscribeEl;
return;
}
@ -747,6 +830,8 @@ loop.OTSdkDriver = (function() {
this.dispatcher.dispatch(new sharedActions.ReceivingScreenShare({
receiving: false
}));
delete this._mockScreenShareEl;
},
/**
@ -754,6 +839,7 @@ loop.OTSdkDriver = (function() {
*/
_onLocalStreamDestroyed: function() {
this._notifyMetricsEvent("Publisher.streamDestroyed");
delete this._mockPublisherEl;
},
/**
@ -793,6 +879,8 @@ loop.OTSdkDriver = (function() {
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.MEDIA_DENIED
}));
delete this._mockPublisherEl;
},
_onOTException: function(event) {
@ -804,6 +892,7 @@ loop.OTSdkDriver = (function() {
this.publisher.off("accessAllowed accessDenied accessDialogOpened streamCreated");
this.publisher.destroy();
delete this.publisher;
delete this._mockPublisherEl;
}
this.dispatcher.dispatch(new sharedActions.ConnectionFailure({
reason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA
@ -824,6 +913,42 @@ loop.OTSdkDriver = (function() {
}
},
/**
* Handle the (remote) VideoEnabled event from the subscriber object
* by dispatching an action with the (hidden) video element from
* which to copy the stream when attaching it to visible video element
* that the views control directly.
*
* @param event {OT.VideoEnabledChangedEvent} from the SDK
*
* @see https://tokbox.com/opentok/libraries/client/js/reference/VideoEnabledChangedEvent.html
* @private
*/
_onVideoEnabled: function(event) {
var sdkSubscriberVideo = this._mockSubscribeEl.querySelector("video");
if (!sdkSubscriberVideo) {
console.error("sdkSubscriberVideo unexpectedly falsy!");
}
this.dispatcher.dispatch(
new sharedActions.RemoteVideoEnabled(
{srcVideoObject: sdkSubscriberVideo}));
},
/**
* Handle the SDK disabling of remote video by dispatching the
* appropriate event.
*
* @param event {OT.VideoEnabledChangedEvent) from the SDK
*
* @see https://tokbox.com/opentok/libraries/client/js/reference/VideoEnabledChangedEvent.html
* @private
*/
_onVideoDisabled: function(event) {
this.dispatcher.dispatch(
new sharedActions.RemoteVideoDisabled());
},
/**
* Publishes the local stream if the session is connected
* and the publisher is ready.
@ -868,6 +993,7 @@ loop.OTSdkDriver = (function() {
this.dispatcher.dispatch(new sharedActions.ScreenSharingState({
state: SCREEN_SHARE_STATES.INACTIVE
}));
delete this._mockScreenSharePreviewEl;
},
/**

View File

@ -678,13 +678,132 @@ loop.shared.views = (function(_, l10n) {
}
});
/**
* Renders an avatar element for display when video is muted.
*/
var AvatarView = React.createClass({displayName: "AvatarView",
mixins: [React.addons.PureRenderMixin],
render: function() {
return React.createElement("div", {className: "avatar"});
}
});
/**
* Renders a media element for display. This also handles displaying an avatar
* instead of the video, and attaching a video stream to the video element.
*/
var MediaView = React.createClass({displayName: "MediaView",
// srcVideoObject should be ok for a shallow comparison, so we are safe
// to use the pure render mixin here.
mixins: [React.addons.PureRenderMixin],
PropTypes: {
displayAvatar: React.PropTypes.bool.isRequired,
posterUrl: React.PropTypes.string,
// Expecting "local" or "remote".
mediaType: React.PropTypes.string.isRequired,
srcVideoObject: React.PropTypes.object
},
componentDidMount: function() {
if (!this.props.displayAvatar) {
this.attachVideo(this.props.srcVideoObject);
}
},
componentDidUpdate: function() {
if (!this.props.displayAvatar) {
this.attachVideo(this.props.srcVideoObject);
}
},
/**
* Attaches a video stream from a donor video element to this component's
* video element if the component is displaying one.
*
* @param {Object} srcVideoObject The src video object to clone the stream
* from.
*
* XXX need to have a corresponding detachVideo or change this to syncVideo
* to protect from leaks (bug 1171978)
*/
attachVideo: function(srcVideoObject) {
if (!srcVideoObject) {
// Not got anything to display.
return;
}
var videoElement = this.getDOMNode();
if (videoElement.tagName.toLowerCase() !== "video") {
// Must be displaying the avatar view, so don't try and attach video.
return;
}
// Set the src of our video element
var attrName = "";
if ("srcObject" in videoElement) {
// srcObject is according to the standard.
attrName = "srcObject";
} else if ("mozSrcObject" in videoElement) {
// mozSrcObject is for Firefox
attrName = "mozSrcObject";
} else if ("src" in videoElement) {
// src is for Chrome.
attrName = "src";
} else {
console.error("Error attaching stream to element - no supported attribute found");
return;
}
// If the object hasn't changed it, then don't reattach it.
if (videoElement[attrName] !== srcVideoObject[attrName]) {
videoElement[attrName] = srcVideoObject[attrName];
}
videoElement.play();
},
render: function() {
if (this.props.displayAvatar) {
return React.createElement(AvatarView, null);
}
if (!this.props.srcVideoObject && !this.props.posterUrl) {
return React.createElement("div", {className: "no-video"});
}
var optionalPoster = {};
if (this.props.posterUrl) {
optionalPoster.poster = this.props.posterUrl;
}
// For now, always mute media. For local media, we should be muted anyway,
// as we don't want to hear ourselves speaking.
//
// For remote media, we would ideally have this live video element in
// control of the audio, but due to the current method of not rendering
// the element at all when video is muted we have to rely on the hidden
// dom element in the sdk driver to play the audio.
// We might want to consider changing this if we add UI controls relating
// to the remote audio at some stage in the future.
return (
React.createElement("video", React.__spread({}, optionalPoster,
{className: this.props.mediaType + "-video",
muted: true}))
);
}
});
return {
AvatarView: AvatarView,
Button: Button,
ButtonGroup: ButtonGroup,
Checkbox: Checkbox,
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
MediaView: MediaView,
ScreenShareControlButton: ScreenShareControlButton,
NotificationListView: NotificationListView
};

View File

@ -678,13 +678,132 @@ loop.shared.views = (function(_, l10n) {
}
});
/**
* Renders an avatar element for display when video is muted.
*/
var AvatarView = React.createClass({
mixins: [React.addons.PureRenderMixin],
render: function() {
return <div className="avatar"/>;
}
});
/**
* Renders a media element for display. This also handles displaying an avatar
* instead of the video, and attaching a video stream to the video element.
*/
var MediaView = React.createClass({
// srcVideoObject should be ok for a shallow comparison, so we are safe
// to use the pure render mixin here.
mixins: [React.addons.PureRenderMixin],
PropTypes: {
displayAvatar: React.PropTypes.bool.isRequired,
posterUrl: React.PropTypes.string,
// Expecting "local" or "remote".
mediaType: React.PropTypes.string.isRequired,
srcVideoObject: React.PropTypes.object
},
componentDidMount: function() {
if (!this.props.displayAvatar) {
this.attachVideo(this.props.srcVideoObject);
}
},
componentDidUpdate: function() {
if (!this.props.displayAvatar) {
this.attachVideo(this.props.srcVideoObject);
}
},
/**
* Attaches a video stream from a donor video element to this component's
* video element if the component is displaying one.
*
* @param {Object} srcVideoObject The src video object to clone the stream
* from.
*
* XXX need to have a corresponding detachVideo or change this to syncVideo
* to protect from leaks (bug 1171978)
*/
attachVideo: function(srcVideoObject) {
if (!srcVideoObject) {
// Not got anything to display.
return;
}
var videoElement = this.getDOMNode();
if (videoElement.tagName.toLowerCase() !== "video") {
// Must be displaying the avatar view, so don't try and attach video.
return;
}
// Set the src of our video element
var attrName = "";
if ("srcObject" in videoElement) {
// srcObject is according to the standard.
attrName = "srcObject";
} else if ("mozSrcObject" in videoElement) {
// mozSrcObject is for Firefox
attrName = "mozSrcObject";
} else if ("src" in videoElement) {
// src is for Chrome.
attrName = "src";
} else {
console.error("Error attaching stream to element - no supported attribute found");
return;
}
// If the object hasn't changed it, then don't reattach it.
if (videoElement[attrName] !== srcVideoObject[attrName]) {
videoElement[attrName] = srcVideoObject[attrName];
}
videoElement.play();
},
render: function() {
if (this.props.displayAvatar) {
return <AvatarView />;
}
if (!this.props.srcVideoObject && !this.props.posterUrl) {
return <div className="no-video"/>;
}
var optionalPoster = {};
if (this.props.posterUrl) {
optionalPoster.poster = this.props.posterUrl;
}
// For now, always mute media. For local media, we should be muted anyway,
// as we don't want to hear ourselves speaking.
//
// For remote media, we would ideally have this live video element in
// control of the audio, but due to the current method of not rendering
// the element at all when video is muted we have to rely on the hidden
// dom element in the sdk driver to play the audio.
// We might want to consider changing this if we add UI controls relating
// to the remote audio at some stage in the future.
return (
<video {...optionalPoster}
className={this.props.mediaType + "-video"}
muted />
);
}
});
return {
AvatarView: AvatarView,
Button: Button,
ButtonGroup: ButtonGroup,
Checkbox: Checkbox,
ConversationView: ConversationView,
ConversationToolbar: ConversationToolbar,
MediaControlButton: MediaControlButton,
MediaView: MediaView,
ScreenShareControlButton: ScreenShareControlButton,
NotificationListView: NotificationListView
};

View File

@ -122,7 +122,7 @@ XPCOMUtils.defineLazyServiceGetter(this, "gWM",
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
let consoleOptions = {
maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
maxLogLevelPref: PREF_LOG_LEVEL,
prefix: "Loop"
};
return new ConsoleAPI(consoleOptions);

View File

@ -131,7 +131,6 @@ loop.StandaloneMozLoop = (function(mozL10n) {
},
async: async,
success: function(responseData) {
console.log("done");
try {
callback(null, validate(responseData, expectedProps));
} catch (err) {

View File

@ -337,7 +337,11 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
isFirefox: React.PropTypes.bool.isRequired
isFirefox: React.PropTypes.bool.isRequired,
// The poster URLs are for UI-showcase testing and development
localPosterUrl: React.PropTypes.string,
remotePosterUrl: React.PropTypes.string,
screenSharePosterUrl: React.PropTypes.string
},
getInitialState: function() {
@ -385,10 +389,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true})
}));
}
@ -411,8 +412,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
// Remove the custom screenshare styles on the remote camera.
var node = this._getElement(".remote");
node.removeAttribute("style");
}
// Force the video sizes to update.
if (this.state.receivingScreenShare != nextState.receivingScreenShare ||
this.state.remoteVideoEnabled != nextState.remoteVideoEnabled) {
this.updateVideoContainer();
}
},
@ -425,6 +428,32 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
},
/**
* Wrapper for window.matchMedia so that we use an appropriate version
* for the ui-showcase, which puts views inside of their own iframes.
*
* Currently, we use an icky hack, and the showcase conspires with
* react-frame-component to set iframe.contentWindow.matchMedia onto
* activeRoomStore. 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.
*
* @param queryString
* @returns {MediaQueryList|null}
* @private
*/
_matchMedia: function(queryString) {
if ("matchMedia" in this.state) {
return this.state.matchMedia(queryString);
} else if ("matchMedia" in window) {
return window.matchMedia(queryString);
}
return null;
},
/**
* Toggles streaming status for a given stream type.
*
@ -458,7 +487,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
var targetWidth;
node.style.right = "auto";
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
if (this._matchMedia("screen and (max-width:640px)").matches) {
// For reduced screen widths, we just go for a fixed size and no overlap.
targetWidth = 180;
node.style.width = (targetWidth * ratio.width) + "px";
@ -470,8 +499,25 @@ loop.standaloneRoomViews = (function(mozL10n) {
// Now position the local camera view correctly with respect to the remote
// video stream or the screen share stream.
var remoteVideoDimensions = this.getRemoteVideoDimensions(
this.state.receivingScreenShare ? "screen" : "camera");
var remoteVideoDimensions;
var isScreenShare = this.state.receivingScreenShare;
var videoDisplayed = isScreenShare ?
this.state.screenShareVideoObject || this.props.screenSharePosterUrl :
this.state.remoteSrcVideoObject || this.props.remotePosterUrl;
if ((isScreenShare || this.shouldRenderRemoteVideo()) && videoDisplayed) {
remoteVideoDimensions = this.getRemoteVideoDimensions(
isScreenShare ? "screen" : "camera");
} else {
var remoteElement = this.getDOMNode().querySelector(".remote.focus-stream");
if (!remoteElement) {
return;
}
remoteVideoDimensions = {
streamWidth: remoteElement.offsetWidth,
offsetX: remoteElement.offsetLeft
};
}
targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
@ -515,7 +561,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
}
// XXX For the time being, if we're a narrow screen, aka mobile, we don't display
// the remote media (bug 1133534).
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
if (this._matchMedia("screen and (max-width:640px)").matches) {
return;
}
@ -557,9 +603,51 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
},
/**
* Works out if remote video should be rended or not, depending on the
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
return true;
}
if (this.state.mediaConnected) {
// since the remoteVideo hasn't yet been enabled, if the
// media is connected, then we should be displaying an avatar.
return false;
}
return true;
case ROOM_STATES.READY:
case ROOM_STATES.INIT:
case ROOM_STATES.JOINING:
case ROOM_STATES.SESSION_CONNECTED:
case ROOM_STATES.JOINED:
case ROOM_STATES.MEDIA_WAIT:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
case ROOM_STATES.CLOSING:
// the other person has shown up, so we don't want to show an avatar
return true;
default:
console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;
}
},
render: function() {
var localStreamClasses = React.addons.classSet({
hide: !this._roomIsActive(),
local: true,
"local-stream": true,
"local-stream-audio": this.state.videoMuted
@ -602,10 +690,25 @@ loop.standaloneRoomViews = (function(mozL10n) {
mozL10n.get("self_view_hidden_message")
),
React.createElement("div", {className: "video_wrapper remote_wrapper"},
React.createElement("div", {className: remoteStreamClasses}),
React.createElement("div", {className: screenShareStreamClasses})
React.createElement("div", {className: remoteStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: !this.shouldRenderRemoteVideo(),
posterUrl: this.props.remotePosterUrl,
mediaType: "remote",
srcVideoObject: this.state.remoteSrcVideoObject})
),
React.createElement("div", {className: localStreamClasses})
React.createElement("div", {className: screenShareStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: false,
posterUrl: this.props.screenSharePosterUrl,
mediaType: "screen-share",
srcVideoObject: this.state.screenShareVideoObject})
)
),
React.createElement("div", {className: localStreamClasses},
React.createElement(sharedViews.MediaView, {displayAvatar: this.state.videoMuted,
posterUrl: this.props.localPosterUrl,
mediaType: "local",
srcVideoObject: this.state.localSrcVideoObject})
)
),
React.createElement(sharedViews.ConversationToolbar, {
dispatcher: this.props.dispatcher,

View File

@ -337,7 +337,11 @@ loop.standaloneRoomViews = (function(mozL10n) {
React.PropTypes.instanceOf(loop.store.FxOSActiveRoomStore)
]).isRequired,
dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired,
isFirefox: React.PropTypes.bool.isRequired
isFirefox: React.PropTypes.bool.isRequired,
// The poster URLs are for UI-showcase testing and development
localPosterUrl: React.PropTypes.string,
remotePosterUrl: React.PropTypes.string,
screenSharePosterUrl: React.PropTypes.string
},
getInitialState: function() {
@ -385,10 +389,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
if (this.state.roomState !== ROOM_STATES.MEDIA_WAIT &&
nextState.roomState === ROOM_STATES.MEDIA_WAIT) {
this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true}),
getLocalElementFunc: this._getElement.bind(this, ".local"),
getRemoteElementFunc: this._getElement.bind(this, ".remote"),
getScreenShareElementFunc: this._getElement.bind(this, ".screen")
publisherConfig: this.getDefaultPublisherConfig({publishVideo: true})
}));
}
@ -411,8 +412,10 @@ loop.standaloneRoomViews = (function(mozL10n) {
// Remove the custom screenshare styles on the remote camera.
var node = this._getElement(".remote");
node.removeAttribute("style");
}
// Force the video sizes to update.
if (this.state.receivingScreenShare != nextState.receivingScreenShare ||
this.state.remoteVideoEnabled != nextState.remoteVideoEnabled) {
this.updateVideoContainer();
}
},
@ -425,6 +428,32 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.props.dispatcher.dispatch(new sharedActions.LeaveRoom());
},
/**
* Wrapper for window.matchMedia so that we use an appropriate version
* for the ui-showcase, which puts views inside of their own iframes.
*
* Currently, we use an icky hack, and the showcase conspires with
* react-frame-component to set iframe.contentWindow.matchMedia onto
* activeRoomStore. 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.
*
* @param queryString
* @returns {MediaQueryList|null}
* @private
*/
_matchMedia: function(queryString) {
if ("matchMedia" in this.state) {
return this.state.matchMedia(queryString);
} else if ("matchMedia" in window) {
return window.matchMedia(queryString);
}
return null;
},
/**
* Toggles streaming status for a given stream type.
*
@ -458,7 +487,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
var targetWidth;
node.style.right = "auto";
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
if (this._matchMedia("screen and (max-width:640px)").matches) {
// For reduced screen widths, we just go for a fixed size and no overlap.
targetWidth = 180;
node.style.width = (targetWidth * ratio.width) + "px";
@ -470,8 +499,25 @@ loop.standaloneRoomViews = (function(mozL10n) {
// Now position the local camera view correctly with respect to the remote
// video stream or the screen share stream.
var remoteVideoDimensions = this.getRemoteVideoDimensions(
this.state.receivingScreenShare ? "screen" : "camera");
var remoteVideoDimensions;
var isScreenShare = this.state.receivingScreenShare;
var videoDisplayed = isScreenShare ?
this.state.screenShareVideoObject || this.props.screenSharePosterUrl :
this.state.remoteSrcVideoObject || this.props.remotePosterUrl;
if ((isScreenShare || this.shouldRenderRemoteVideo()) && videoDisplayed) {
remoteVideoDimensions = this.getRemoteVideoDimensions(
isScreenShare ? "screen" : "camera");
} else {
var remoteElement = this.getDOMNode().querySelector(".remote.focus-stream");
if (!remoteElement) {
return;
}
remoteVideoDimensions = {
streamWidth: remoteElement.offsetWidth,
offsetX: remoteElement.offsetLeft
};
}
targetWidth = remoteVideoDimensions.streamWidth * LOCAL_STREAM_SIZE;
@ -515,7 +561,7 @@ loop.standaloneRoomViews = (function(mozL10n) {
}
// XXX For the time being, if we're a narrow screen, aka mobile, we don't display
// the remote media (bug 1133534).
if (window.matchMedia && window.matchMedia("screen and (max-width:640px)").matches) {
if (this._matchMedia("screen and (max-width:640px)").matches) {
return;
}
@ -557,9 +603,51 @@ loop.standaloneRoomViews = (function(mozL10n) {
this.state.roomState === ROOM_STATES.HAS_PARTICIPANTS;
},
/**
* Works out if remote video should be rended or not, depending on the
* room state and other flags.
*
* @return {Boolean} True if remote video should be rended.
*/
shouldRenderRemoteVideo: function() {
switch(this.state.roomState) {
case ROOM_STATES.HAS_PARTICIPANTS:
if (this.state.remoteVideoEnabled) {
return true;
}
if (this.state.mediaConnected) {
// since the remoteVideo hasn't yet been enabled, if the
// media is connected, then we should be displaying an avatar.
return false;
}
return true;
case ROOM_STATES.READY:
case ROOM_STATES.INIT:
case ROOM_STATES.JOINING:
case ROOM_STATES.SESSION_CONNECTED:
case ROOM_STATES.JOINED:
case ROOM_STATES.MEDIA_WAIT:
// this case is so that we don't show an avatar while waiting for
// the other party to connect
return true;
case ROOM_STATES.CLOSING:
// the other person has shown up, so we don't want to show an avatar
return true;
default:
console.warn("StandaloneRoomView.shouldRenderRemoteVideo:" +
" unexpected roomState: ", this.state.roomState);
return true;
}
},
render: function() {
var localStreamClasses = React.addons.classSet({
hide: !this._roomIsActive(),
local: true,
"local-stream": true,
"local-stream-audio": this.state.videoMuted
@ -602,10 +690,25 @@ loop.standaloneRoomViews = (function(mozL10n) {
{mozL10n.get("self_view_hidden_message")}
</span>
<div className="video_wrapper remote_wrapper">
<div className={remoteStreamClasses}></div>
<div className={screenShareStreamClasses}></div>
<div className={remoteStreamClasses}>
<sharedViews.MediaView displayAvatar={!this.shouldRenderRemoteVideo()}
posterUrl={this.props.remotePosterUrl}
mediaType="remote"
srcVideoObject={this.state.remoteSrcVideoObject} />
</div>
<div className={screenShareStreamClasses}>
<sharedViews.MediaView displayAvatar={false}
posterUrl={this.props.screenSharePosterUrl}
mediaType="screen-share"
srcVideoObject={this.state.screenShareVideoObject} />
</div>
</div>
<div className={localStreamClasses}>
<sharedViews.MediaView displayAvatar={this.state.videoMuted}
posterUrl={this.props.localPosterUrl}
mediaType="local"
srcVideoObject={this.state.localSrcVideoObject} />
</div>
<div className={localStreamClasses}></div>
</div>
<sharedViews.ConversationToolbar
dispatcher={this.props.dispatcher}

View File

@ -8,8 +8,8 @@ describe("loop.conversationViews", function () {
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedView = loop.shared.views;
var sandbox, view, dispatcher, contact, fakeAudioXHR;
var sharedViews = loop.shared.views;
var sandbox, view, dispatcher, contact, fakeAudioXHR, conversationStore;
var fakeMozLoop, fakeWindow;
var CALL_STATES = loop.store.CALL_STATES;
@ -104,6 +104,19 @@ describe("loop.conversationViews", function () {
};
loop.shared.mixins.setRootObject(fakeWindow);
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
conversationStore = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: fakeMozLoop,
sdkDriver: {}
});
loop.store.StoreMixin.register({
conversationStore: conversationStore,
feedbackStore: feedbackStore
});
});
afterEach(function() {
@ -255,7 +268,7 @@ describe("loop.conversationViews", function () {
});
describe("CallFailedView", function() {
var store, fakeAudio;
var fakeAudio;
var contact = {email: [{value: "test@test.tld"}]};
@ -269,15 +282,6 @@ describe("loop.conversationViews", function () {
}
beforeEach(function() {
store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: navigator.mozLoop,
sdkDriver: {}
});
loop.store.StoreMixin.register({
conversationStore: store
});
fakeAudio = {
play: sinon.spy(),
pause: sinon.spy(),
@ -357,7 +361,7 @@ describe("loop.conversationViews", function () {
it("should compose an email once the email link is received", function() {
var composeCallUrlEmail = sandbox.stub(sharedUtils, "composeCallUrlEmail");
view = mountTestComponent({contact: contact});
store.setStoreState({emailLink: "http://fake.invalid/"});
conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(composeCallUrlEmail);
sinon.assert.calledWithExactly(composeCallUrlEmail,
@ -368,7 +372,7 @@ describe("loop.conversationViews", function () {
function() {
view = mountTestComponent({contact: contact});
store.setStoreState({emailLink: "http://fake.invalid/"});
conversationStore.setStoreState({emailLink: "http://fake.invalid/"});
sinon.assert.calledOnce(fakeWindow.close);
});
@ -377,7 +381,7 @@ describe("loop.conversationViews", function () {
function() {
view = mountTestComponent({contact: contact});
store.trigger("error:emailLink");
conversationStore.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".error")).not.eql(null);
});
@ -386,7 +390,7 @@ describe("loop.conversationViews", function () {
function() {
view = mountTestComponent({contact: contact});
store.trigger("error:emailLink");
conversationStore.trigger("error:emailLink");
expect(view.getDOMNode().querySelector(".btn-email").disabled).eql(false);
});
@ -403,7 +407,7 @@ describe("loop.conversationViews", function () {
it("should show 'something went wrong' when the reason is WEBSOCKET_REASONS.MEDIA_FAIL",
function () {
store.setStoreState({callStateReason: WEBSOCKET_REASONS.MEDIA_FAIL});
conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.MEDIA_FAIL});
view = mountTestComponent({contact: contact});
@ -412,7 +416,7 @@ describe("loop.conversationViews", function () {
it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.REJECT",
function () {
store.setStoreState({callStateReason: WEBSOCKET_REASONS.REJECT});
conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.REJECT});
view = mountTestComponent({contact: contact});
@ -423,7 +427,7 @@ describe("loop.conversationViews", function () {
it("should show 'contact unavailable' when the reason is WEBSOCKET_REASONS.BUSY",
function () {
store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
view = mountTestComponent({contact: contact});
@ -434,7 +438,7 @@ describe("loop.conversationViews", function () {
it("should show 'something went wrong' when the reason is 'setup'",
function () {
store.setStoreState({callStateReason: "setup"});
conversationStore.setStoreState({callStateReason: "setup"});
view = mountTestComponent({contact: contact});
@ -444,7 +448,7 @@ describe("loop.conversationViews", function () {
it("should show 'contact unavailable' when the reason is REST_ERRNOS.USER_UNAVAILABLE",
function () {
store.setStoreState({callStateReason: REST_ERRNOS.USER_UNAVAILABLE});
conversationStore.setStoreState({callStateReason: REST_ERRNOS.USER_UNAVAILABLE});
view = mountTestComponent({contact: contact});
@ -455,7 +459,7 @@ describe("loop.conversationViews", function () {
it("should show 'no media' when the reason is FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA",
function () {
store.setStoreState({callStateReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA});
conversationStore.setStoreState({callStateReason: FAILURE_DETAILS.UNABLE_TO_PUBLISH_MEDIA});
view = mountTestComponent({contact: contact});
@ -464,7 +468,7 @@ describe("loop.conversationViews", function () {
it("should display a generic contact unavailable msg when the reason is" +
" WEBSOCKET_REASONS.BUSY and no display name is available", function() {
store.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
conversationStore.setStoreState({callStateReason: WEBSOCKET_REASONS.BUSY});
var phoneOnlyContact = {
tel: [{"pref": true, type: "work", value: ""}]
};
@ -477,27 +481,72 @@ describe("loop.conversationViews", function () {
});
describe("OngoingConversationView", function() {
function mountTestComponent(props) {
function mountTestComponent(extraProps) {
var props = _.extend({
dispatcher: dispatcher
}, extraProps);
return TestUtils.renderIntoDocument(
React.createElement(loop.conversationViews.OngoingConversationView, props));
}
it("should dispatch a setupStreamElements action when the view is created",
function() {
view = mountTestComponent({
dispatcher: dispatcher
});
view = mountTestComponent();
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithMatch(dispatcher.dispatch,
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 }
});
view = mountTestComponent({
mediaConnected: true,
remoteVideoEnabled: true
});
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 }
});
view = mountTestComponent({
video: {
enabled: true
}
});
expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
});
it("should dispatch a hangupCall action when the hangup button is pressed",
function() {
view = mountTestComponent({
dispatcher: dispatcher
});
view = mountTestComponent();
var hangupBtn = view.getDOMNode().querySelector(".btn-hangup");
@ -510,7 +559,6 @@ describe("loop.conversationViews", function () {
it("should dispatch a setMute action when the audio mute button is pressed",
function() {
view = mountTestComponent({
dispatcher: dispatcher,
audio: {enabled: false}
});
@ -529,7 +577,6 @@ describe("loop.conversationViews", function () {
it("should dispatch a setMute action when the video mute button is pressed",
function() {
view = mountTestComponent({
dispatcher: dispatcher,
video: {enabled: true}
});
@ -547,7 +594,6 @@ describe("loop.conversationViews", function () {
it("should set the mute button as mute off", function() {
view = mountTestComponent({
dispatcher: dispatcher,
video: {enabled: true}
});
@ -558,7 +604,6 @@ describe("loop.conversationViews", function () {
it("should set the mute button as mute on", function() {
view = mountTestComponent({
dispatcher: dispatcher,
audio: {enabled: false}
});
@ -569,7 +614,7 @@ describe("loop.conversationViews", function () {
});
describe("CallControllerView", function() {
var store, feedbackStore;
var feedbackStore;
function mountTestComponent() {
return TestUtils.renderIntoDocument(
@ -580,22 +625,13 @@ describe("loop.conversationViews", function () {
}
beforeEach(function() {
store = new loop.store.ConversationStore(dispatcher, {
client: {},
mozLoop: fakeMozLoop,
sdkDriver: {}
});
loop.store.StoreMixin.register({
conversationStore: store
});
feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: {}
});
});
it("should set the document title to the callerId", function() {
store.setStoreState({
conversationStore.setStoreState({
contact: contact
});
@ -606,7 +642,7 @@ describe("loop.conversationViews", function () {
it("should fallback to the contact email if the contact name is not defined", function() {
delete contact.name;
store.setStoreState({
conversationStore.setStoreState({
contact: contact
});
@ -616,7 +652,7 @@ describe("loop.conversationViews", function () {
});
it("should fallback to the caller id if no contact is defined", function() {
store.setStoreState({
conversationStore.setStoreState({
callerId: "fakeId"
});
@ -627,7 +663,7 @@ describe("loop.conversationViews", function () {
it("should render the CallFailedView when the call state is 'terminated'",
function() {
store.setStoreState({
conversationStore.setStoreState({
callState: CALL_STATES.TERMINATED,
contact: contact
});
@ -640,7 +676,7 @@ describe("loop.conversationViews", function () {
it("should render the PendingConversationView for outgoing calls when the call state is 'gather'",
function() {
store.setStoreState({
conversationStore.setStoreState({
callState: CALL_STATES.GATHER,
contact: contact,
outgoing: true
@ -653,7 +689,7 @@ describe("loop.conversationViews", function () {
});
it("should render the AcceptCallView for incoming calls when the call state is 'alerting'", function() {
store.setStoreState({
conversationStore.setStoreState({
callState: CALL_STATES.ALERTING,
outgoing: false
});
@ -666,7 +702,7 @@ describe("loop.conversationViews", function () {
it("should render the OngoingConversationView when the call state is 'ongoing'",
function() {
store.setStoreState({callState: CALL_STATES.ONGOING});
conversationStore.setStoreState({callState: CALL_STATES.ONGOING});
view = mountTestComponent();
@ -676,7 +712,7 @@ describe("loop.conversationViews", function () {
it("should render the FeedbackView when the call state is 'finished'",
function() {
store.setStoreState({callState: CALL_STATES.FINISHED});
conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
view = mountTestComponent();
@ -685,7 +721,7 @@ describe("loop.conversationViews", function () {
});
it("should set the document title to conversation_has_ended when displaying the feedback view", function() {
store.setStoreState({callState: CALL_STATES.FINISHED});
conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
mountTestComponent();
@ -701,7 +737,7 @@ describe("loop.conversationViews", function () {
};
sandbox.stub(window, "Audio").returns(fakeAudio);
store.setStoreState({callState: CALL_STATES.FINISHED});
conversationStore.setStoreState({callState: CALL_STATES.FINISHED});
view = mountTestComponent();
@ -710,7 +746,7 @@ describe("loop.conversationViews", function () {
it("should update the rendered views when the state is changed.",
function() {
store.setStoreState({
conversationStore.setStoreState({
callState: CALL_STATES.GATHER,
contact: contact,
outgoing: true
@ -721,7 +757,7 @@ describe("loop.conversationViews", function () {
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.PendingConversationView);
store.setStoreState({callState: CALL_STATES.TERMINATED});
conversationStore.setStoreState({callState: CALL_STATES.TERMINATED});
TestUtils.findRenderedComponentWithType(view,
loop.conversationViews.CallFailedView);

View File

@ -34,4 +34,8 @@ describe("document.mozL10n", function() {
it("should get a plural form", function() {
expect(document.mozL10n.get("plural", {num: 10})).eql("10 plural forms");
});
it("should correctly get a plural form for num = 0", function() {
expect(document.mozL10n.get("plural", {num: 0})).eql("0 plural form");
});
});

View File

@ -603,9 +603,10 @@ describe("loop.store.RoomStore", function () {
});
describe("#updateRoomContext", function() {
var store, fakeMozLoop;
var store, fakeMozLoop, clock;
beforeEach(function() {
clock = sinon.useFakeTimers();
fakeMozLoop = {
rooms: {
get: sinon.stub().callsArgWith(1, null, {
@ -620,6 +621,10 @@ describe("loop.store.RoomStore", function () {
store = new loop.store.RoomStore(dispatcher, {mozLoop: fakeMozLoop});
});
afterEach(function() {
clock.restore();
});
it("should rename the room via mozLoop", function() {
fakeMozLoop.rooms.update = sinon.spy();
dispatcher.dispatch(new sharedActions.UpdateRoomContext({
@ -674,6 +679,7 @@ describe("loop.store.RoomStore", function () {
roomToken: "42abc",
newRoomName: " \t \t "
}));
clock.tick(1);
sinon.assert.notCalled(fakeMozLoop.rooms.update);
expect(store.getStoreState().savingContext).to.eql(false);
@ -731,6 +737,7 @@ describe("loop.store.RoomStore", function () {
newRoomThumbnail: "",
newRoomURL: ""
}));
clock.tick(1);
sinon.assert.notCalled(fakeMozLoop.rooms.update);
expect(store.getStoreState().savingContext).to.eql(false);

View File

@ -8,6 +8,7 @@ describe("loop.roomViews", function () {
var TestUtils = React.addons.TestUtils;
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sharedViews = loop.shared.views;
var ROOM_STATES = loop.store.ROOM_STATES;
var SCREEN_SHARE_STATES = loop.shared.utils.SCREEN_SHARE_STATES;
@ -67,6 +68,13 @@ describe("loop.roomViews", function () {
mozLoop: fakeMozLoop,
activeRoomStore: activeRoomStore
});
var textChatStore = new loop.store.TextChatStore(dispatcher, {
sdkDriver: {}
});
loop.store.StoreMixin.register({
textChatStore: textChatStore
});
fakeContextURL = {
description: "An invalid page",
@ -422,16 +430,6 @@ describe("loop.roomViews", function () {
sinon.assert.calledOnce(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
sinon.assert.calledWithExactly(dispatcher.dispatch,
sinon.match(function(value) {
return value.getLocalElementFunc() ===
view.getDOMNode().querySelector(".local");
}));
sinon.assert.calledWithExactly(dispatcher.dispatch,
sinon.match(function(value) {
return value.getRemoteElementFunc() ===
view.getDOMNode().querySelector(".remote");
}));
}
it("should dispatch a `SetupStreamElements` action when the MEDIA_WAIT state " +
@ -516,6 +514,54 @@ describe("loop.roomViews", function () {
TestUtils.findRenderedComponentWithType(view,
loop.shared.views.FeedbackView);
});
it("should display an avatar for remote video when the room has participants but video is not enabled",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
mediaConnected: true,
remoteVideoEnabled: false
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
});
it("should display the remote video when there are participants and video is enabled", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
mediaConnected: true,
remoteVideoEnabled: true,
remoteSrcVideoObject: { fake: 1 }
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
});
it("should display an avatar for local video when the stream is muted", function() {
activeRoomStore.setStoreState({
videoMuted: true
});
view = mountTestComponent();
TestUtils.findRenderedComponentWithType(view, sharedViews.AvatarView);
});
it("should display the local video when the stream is enabled", function() {
activeRoomStore.setStoreState({
localSrcVideoObject: { fake: 1 },
videoMuted: false
});
view = mountTestComponent();
expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
});
});
describe("Mute", function() {
@ -641,6 +687,7 @@ describe("loop.roomViews", function () {
props = _.extend({
dispatcher: dispatcher,
mozLoop: fakeMozLoop,
savingContext: false,
show: true,
roomData: {
roomToken: "fakeToken"
@ -726,7 +773,7 @@ describe("loop.roomViews", function () {
expect(checkbox.classList.contains("disabled")).to.eql(true);
});
it("should render the editMode view when the edit button is clicked", function(next) {
it("should render the editMode view when the edit button is clicked", function(done) {
var roomName = "Hello, is it me you're looking for?";
view = mountTestComponent({
roomData: {
@ -749,11 +796,11 @@ describe("loop.roomViews", function () {
expect(node.querySelector(".room-context-url").value).to.eql(fakeContextURL.location);
expect(node.querySelector(".room-context-comments").value).to.eql(fakeContextURL.description);
next();
done();
});
});
it("should hide the checkbox when no context data is stored or available", function(next) {
it("should hide the checkbox when no context data is stored or available", function(done) {
view = mountTestComponent({
roomData: {
roomToken: "fakeToken",
@ -771,7 +818,7 @@ describe("loop.roomViews", function () {
var node = view.getDOMNode();
expect(node.querySelector(".checkbox-wrapper").classList.contains("hide")).to.eql(true);
next();
done();
});
});
});
@ -831,6 +878,21 @@ describe("loop.roomViews", function () {
newRoomThumbnail: fakeContextURL.thumbnail
}));
});
it("should close the edit form when context was saved successfully", function(done) {
view.setProps({ savingContext: true }, function() {
var node = view.getDOMNode();
// The button should show up as disabled.
expect(node.querySelector(".btn-info").hasAttribute("disabled")).to.eql(true);
// Now simulate a successful save.
view.setProps({ savingContext: false }, function() {
// The editMode flag should be updated.
expect(view.state.editMode).to.eql(false);
done();
});
});
});
});
describe("#handleCheckboxChange", function() {

View File

@ -102,7 +102,7 @@ class Test1BrowserCall(MarionetteTestCase):
media_container = self.wait_for_element_displayed(By.CLASS_NAME, "media")
self.assertEqual(media_container.tag_name, "div", "expect a video container")
self.check_video(".local .OT_publisher .OT_widget-container");
self.check_video(".local-video")
def local_get_and_verify_room_url(self):
self.switch_to_chatbox()
@ -127,23 +127,20 @@ class Test1BrowserCall(MarionetteTestCase):
"btn-join")
join_button.click()
# Assumes the standlone or the conversation window is selected first.
# Assumes the standalone or the conversation window is selected first.
def check_video(self, selector):
video_wrapper = self.wait_for_element_displayed(By.CSS_SELECTOR,
video = self.wait_for_element_displayed(By.CSS_SELECTOR,
selector, 20)
video = self.wait_for_subelement_displayed(video_wrapper,
By.TAG_NAME, "video")
self.wait_for_element_attribute_to_be_false(video, "paused")
self.assertEqual(video.get_attribute("ended"), "false")
def standalone_check_remote_video(self):
self.switch_to_standalone()
self.check_video(".remote .OT_subscriber .OT_widget-container")
self.check_video(".remote-video")
def local_check_remote_video(self):
self.switch_to_chatbox()
self.check_video(".remote .OT_subscriber .OT_widget-container")
self.check_video(".remote-video")
def local_enable_screenshare(self):
self.switch_to_chatbox()
@ -153,7 +150,7 @@ class Test1BrowserCall(MarionetteTestCase):
def standalone_check_remote_screenshare(self):
self.switch_to_standalone()
self.check_video(".media .screen .OT_subscriber .OT_widget-container")
self.check_video(".screen-share-video")
def remote_leave_room_and_verify_feedback(self):
self.switch_to_standalone()

View File

@ -971,6 +971,61 @@ describe("loop.store.ActiveRoomStore", function () {
});
});
describe("#localVideoEnabled", function() {
it("should add a localSrcVideoObject to the store", function() {
var fakeVideoElement = {name: "fakeVideoElement"};
expect(store.getStoreState()).to.not.have.property("localSrcVideoObject");
store.localVideoEnabled({srcVideoObject: fakeVideoElement});
expect(store.getStoreState()).to.have.property("localSrcVideoObject",
fakeVideoElement);
});
});
describe("#remoteVideoEnabled", function() {
var fakeVideoElement;
beforeEach(function() {
fakeVideoElement = {name: "fakeVideoElement"};
});
it("should add a remoteSrcVideoObject to the store", function() {
expect(store.getStoreState()).to.not.have.property("remoteSrcVideoObject");
store.remoteVideoEnabled({srcVideoObject: fakeVideoElement});
expect(store.getStoreState()).to.have.property("remoteSrcVideoObject",
fakeVideoElement);
});
it("should set remoteVideoEnabled", function() {
store.remoteVideoEnabled({srcVideoObject: fakeVideoElement});
expect(store.getStoreState().remoteVideoEnabled).eql(true);
});
});
describe("#remoteVideoDisabled", function() {
it("should set remoteVideoEnabled to false", function() {
store.setStoreState({
remoteVideoEnabled: true
});
store.remoteVideoDisabled();
expect(store.getStoreState().remoteVideoEnabled).eql(false);
});
});
describe("#mediaConnected", function() {
it("should set mediaConnected to true", function() {
store.mediaConnected();
expect(store.getStoreState().mediaConnected).eql(true);
});
});
describe("#screenSharingState", function() {
beforeEach(function() {
store.setStoreState({windowId: "1234"});
@ -1012,6 +1067,34 @@ describe("loop.store.ActiveRoomStore", function () {
expect(store.getStoreState().receivingScreenShare).eql(true);
});
it("should add a screenShareVideoObject to the store when sharing is active", function() {
var fakeVideoElement = {name: "fakeVideoElement"};
expect(store.getStoreState()).to.not.have.property("screenShareVideoObject");
store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
receiving: true,
srcVideoObject: fakeVideoElement
}));
expect(store.getStoreState()).to.have.property("screenShareVideoObject",
fakeVideoElement);
});
it("should clear the screenShareVideoObject from the store when sharing is inactive", function() {
store.setStoreState({
screenShareVideoObject: {
name: "fakeVideoElement"
}
});
store.receivingScreenShare(new sharedActions.ReceivingScreenShare({
receiving: false,
srcVideoObject: null
}));
expect(store.getStoreState().screenShareVideoObject).eql(null);
});
it("should delete the screen remote video dimensions if screen sharing is not active", function() {
store.setStoreState({
remoteVideoDimensions: {
@ -1162,6 +1245,16 @@ describe("loop.store.ActiveRoomStore", function () {
expect(store.getStoreState().roomState).eql(ROOM_STATES.SESSION_CONNECTED);
});
it("should clear the remoteSrcVideoObject", function() {
store.setStoreState({
remoteSrcVideoObject: { name: "fakeVideoElement" }
});
store.remotePeerDisconnected();
expect(store.getStoreState().remoteSrcVideoObject).eql(null);
});
});
describe("#connectionStatus", function() {

View File

@ -13,7 +13,7 @@ describe("loop.store.ConversationStore", function () {
var sharedActions = loop.shared.actions;
var sharedUtils = loop.shared.utils;
var sandbox, dispatcher, client, store, fakeSessionData, sdkDriver;
var contact, fakeMozLoop;
var contact, fakeMozLoop, fakeVideoElement;
var connectPromise, resolveConnectPromise, rejectConnectPromise;
var wsCancelSpy, wsCloseSpy, wsDeclineSpy, wsMediaUpSpy, fakeWebsocket;
@ -89,6 +89,8 @@ describe("loop.store.ConversationStore", function () {
progressURL: "fakeURL"
};
fakeVideoElement = { id: "fakeVideoElement" };
var dummySocket = {
close: sinon.spy(),
send: sinon.spy()
@ -927,6 +929,62 @@ describe("loop.store.ConversationStore", function () {
sinon.assert.calledOnce(wsMediaUpSpy);
});
it("should set store.mediaConnected to true", function () {
store._websocket = fakeWebsocket;
store.mediaConnected(new sharedActions.MediaConnected());
expect(store.getStoreState("mediaConnected")).eql(true);
});
});
describe("#localVideoEnabled", function() {
it("should set store.localSrcVideoObject from the action data", function () {
store.localVideoEnabled(
new sharedActions.LocalVideoEnabled({srcVideoObject: fakeVideoElement}));
expect(store.getStoreState("localSrcVideoObject")).eql(fakeVideoElement);
});
});
describe("#remoteVideoEnabled", function() {
it("should set store.remoteSrcVideoObject from the actionData", function () {
store.setStoreState({remoteSrcVideoObject: undefined});
store.remoteVideoEnabled(
new sharedActions.RemoteVideoEnabled({srcVideoObject: fakeVideoElement}));
expect(store.getStoreState("remoteSrcVideoObject")).eql(fakeVideoElement);
});
it("should set store.remoteVideoEnabled to true", function () {
store.setStoreState({remoteVideoEnabled: false});
store.remoteVideoEnabled(
new sharedActions.RemoteVideoEnabled({srcVideoObject: fakeVideoElement}));
expect(store.getStoreState("remoteVideoEnabled")).to.be.true;
});
});
describe("#remoteVideoDisabled", function() {
it("should set store.remoteVideoEnabled to false", function () {
store.setStoreState({remoteVideoEnabled: true});
store.remoteVideoDisabled(new sharedActions.RemoteVideoDisabled({}));
expect(store.getStoreState("remoteVideoEnabled")).to.be.false;
});
it("should set store.remoteSrcVideoObject to undefined", function () {
store.setStoreState({remoteSrcVideoObject: fakeVideoElement});
store.remoteVideoDisabled(new sharedActions.RemoteVideoDisabled({}));
expect(store.getStoreState("remoteSrcVideoObject")).to.be.undefined;
});
});
describe("#setMute", function() {

View File

@ -368,88 +368,6 @@ describe("loop.shared.mixins", function() {
});
describe("Events", function() {
describe("resize", function() {
it("should update the width on the local stream element", function() {
localElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { width: "0%" }
};
rootObject.events.resize();
sandbox.clock.tick(10);
expect(localElement.style.width).eql("100%");
});
it("should update the height on the remote stream element", function() {
remoteElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.resize();
sandbox.clock.tick(10);
expect(remoteElement.style.height).eql("100%");
});
it("should update the height on the screen share stream element", function() {
screenShareElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.resize();
sandbox.clock.tick(10);
expect(screenShareElement.style.height).eql("100%");
});
});
describe("orientationchange", function() {
it("should update the width on the local stream element", function() {
localElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { width: "0%" }
};
rootObject.events.orientationchange();
sandbox.clock.tick(10);
expect(localElement.style.width).eql("100%");
});
it("should update the height on the remote stream element", function() {
remoteElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.orientationchange();
sandbox.clock.tick(10);
expect(remoteElement.style.height).eql("100%");
});
it("should update the height on the screen share stream element", function() {
screenShareElement = {
offsetWidth: 100,
offsetHeight: 100,
style: { height: "0%" }
};
rootObject.events.orientationchange();
sandbox.clock.tick(10);
expect(screenShareElement.style.height).eql("100%");
});
});
describe("Video stream dimensions", function() {
var localVideoDimensions = {

View File

@ -13,15 +13,11 @@ describe("loop.OTSdkDriver", function () {
var sandbox;
var dispatcher, driver, mozLoop, publisher, sdk, session, sessionData, subscriber;
var fakeLocalElement, fakeRemoteElement, fakeScreenElement;
var publisherConfig, fakeEvent;
beforeEach(function() {
sandbox = sinon.sandbox.create();
fakeLocalElement = { fake: 1 };
fakeRemoteElement = { fake: 2 };
fakeScreenElement = { fake: 3 };
fakeEvent = {
preventDefault: sinon.stub()
};
@ -120,8 +116,6 @@ describe("loop.OTSdkDriver", function () {
describe("#setupStreamElements", function() {
it("should call initPublisher", function() {
driver.setupStreamElements(new sharedActions.SetupStreamElements({
getLocalElementFunc: function() { return fakeLocalElement; },
getRemoteElementFunc: function() { return fakeRemoteElement; },
publisherConfig: publisherConfig
}));
@ -132,7 +126,9 @@ describe("loop.OTSdkDriver", function () {
}, publisherConfig);
sinon.assert.calledOnce(sdk.initPublisher);
sinon.assert.calledWith(sdk.initPublisher, fakeLocalElement, expectedConfig);
sinon.assert.calledWith(sdk.initPublisher,
sinon.match.instanceOf(HTMLDivElement),
expectedConfig);
});
});
@ -141,8 +137,6 @@ describe("loop.OTSdkDriver", function () {
sdk.initPublisher.returns(publisher);
driver.setupStreamElements(new sharedActions.SetupStreamElements({
getLocalElementFunc: function() { return fakeLocalElement; },
getRemoteElementFunc: function() { return fakeRemoteElement; },
publisherConfig: publisherConfig
}));
});
@ -169,7 +163,9 @@ describe("loop.OTSdkDriver", function () {
}, publisherConfig);
sinon.assert.calledTwice(sdk.initPublisher);
sinon.assert.calledWith(sdk.initPublisher, fakeLocalElement, expectedConfig);
sinon.assert.calledWith(sdk.initPublisher,
sinon.match.instanceOf(HTMLDivElement),
expectedConfig);
});
});
@ -178,8 +174,6 @@ describe("loop.OTSdkDriver", function () {
sdk.initPublisher.returns(publisher);
driver.setupStreamElements(new sharedActions.SetupStreamElements({
getLocalElementFunc: function() { return fakeLocalElement; },
getRemoteElementFunc: function() { return fakeRemoteElement; },
publisherConfig: publisherConfig
}));
});
@ -206,18 +200,8 @@ describe("loop.OTSdkDriver", function () {
});
describe("#startScreenShare", function() {
var fakeElement;
beforeEach(function() {
sandbox.stub(driver, "_noteSharingState");
fakeElement = {
className: "fakeVideo"
};
driver.getScreenShareElementFunc = function() {
return fakeElement;
};
});
it("should initialize a publisher", function() {
@ -233,7 +217,8 @@ describe("loop.OTSdkDriver", function () {
driver.startScreenShare(options);
sinon.assert.calledOnce(sdk.initPublisher);
sinon.assert.calledWithMatch(sdk.initPublisher, fakeElement, options);
sinon.assert.calledWithMatch(sdk.initPublisher,
sinon.match.instanceOf(HTMLDivElement), options);
});
it("should log a telemetry action", function() {
@ -259,10 +244,6 @@ describe("loop.OTSdkDriver", function () {
scrollWithPage: true
}
};
driver.getScreenShareElementFunc = function() {
return fakeScreenElement;
};
driver.startScreenShare(options);
});
@ -282,8 +263,6 @@ describe("loop.OTSdkDriver", function () {
describe("#endScreenShare", function() {
beforeEach(function() {
driver.getScreenShareElementFunc = function() {};
sandbox.stub(driver, "_noteSharingState");
});
@ -638,14 +617,34 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("Events (general media)", function() {
describe("Events: general media", function() {
var fakeConnection, fakeStream, fakeSubscriberObject,
fakeSdkContainerWithVideo, videoElement;
beforeEach(function() {
fakeConnection = "fakeConnection";
fakeStream = {
hasVideo: true,
videoType: "camera",
videoDimensions: {width: 1, height: 2}
};
fakeSubscriberObject = _.extend({
session: { connection: fakeConnection },
stream: fakeStream
}, Backbone.Events);
fakeSdkContainerWithVideo = {
querySelector: sinon.stub().returns(videoElement)
};
// use a real video element so that these tests correctly reflect
// test behavior when run in firefox or chrome
videoElement = document.createElement("video");
driver.connectSession(sessionData);
driver.setupStreamElements(new sharedActions.SetupStreamElements({
getLocalElementFunc: function() {return fakeLocalElement; },
getScreenShareElementFunc: function() {return fakeScreenElement; },
getRemoteElementFunc: function() {return fakeRemoteElement; },
publisherConfig: publisherConfig
}));
});
@ -760,9 +759,13 @@ describe("loop.OTSdkDriver", function () {
});
describe("streamCreated (publisher/local)", function() {
var fakeStream;
var fakeStream, fakeMockVideo;
beforeEach(function() {
driver._mockPublisherEl = document.createElement("div");
fakeMockVideo = document.createElement("video");
driver._mockPublisherEl.appendChild(fakeMockVideo);
fakeStream = {
hasVideo: true,
videoType: "camera",
@ -782,6 +785,16 @@ describe("loop.OTSdkDriver", function () {
}));
});
it("should dispatch a LocalVideoEnabled action", function() {
publisher.trigger("streamCreated", { stream: fakeStream });
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.LocalVideoEnabled({
srcVideoObject: fakeMockVideo
}));
});
it("should dispatch a ConnectionStatus action", function() {
driver._metrics.recvStreams = 1;
driver._metrics.connections = 2;
@ -800,16 +813,7 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("streamCreated (session/remote)", function() {
var fakeStream;
beforeEach(function() {
fakeStream = {
hasVideo: true,
videoType: "camera",
videoDimensions: {width: 1, height: 2}
};
});
describe("streamCreated: session/remote", function() {
it("should dispatch a VideoDimensionsChanged action", function() {
session.trigger("streamCreated", { stream: fakeStream });
@ -843,28 +847,63 @@ describe("loop.OTSdkDriver", function () {
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.calledOnce(session.subscribe);
sinon.assert.calledWith(session.subscribe,
fakeStream, fakeRemoteElement, publisherConfig);
sinon.assert.calledWithExactly(session.subscribe,
fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
sinon.match.func);
});
it("should dispatch RemoteVideoEnabled if the stream has video" +
" after subscribe is complete", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement).returns(this.fakeSubscriberObject);
driver.session = session;
fakeStream.connection = fakeConnection;
fakeStream.hasVideo = true;
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RemoteVideoEnabled({
srcVideoObject: videoElement
}));
});
it("should not dispatch RemoteVideoEnabled if the stream is audio-only", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
fakeStream.connection = fakeConnection;
fakeStream.hasVideo = false;
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.called(dispatcher.dispatch);
sinon.assert.neverCalledWith(dispatcher.dispatch,
new sharedActions.RemoteVideoEnabled({
srcVideoObject: videoElement
}));
});
it("should trigger a readyForDataChannel signal after subscribe is complete", function() {
session.subscribe.callsArgWith(3, null);
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
document.createElement("video"));
driver._useDataChannels = true;
fakeStream.connection = "fakeID";
fakeStream.connection = fakeConnection;
session.trigger("streamCreated", { stream: fakeStream });
sinon.assert.calledOnce(session.signal);
sinon.assert.calledWith(session.signal, {
type: "readyForDataChannel",
to: "fakeID"
to: fakeConnection
});
});
it("should not trigger readyForDataChannel signal if data channels are not wanted", function() {
session.subscribe.callsArgWith(3, null);
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
document.createElement("video"));
driver._useDataChannels = false;
fakeStream.connection = "fakeID";
fakeStream.connection = fakeConnection;
session.trigger("streamCreated", { stream: fakeStream });
@ -878,10 +917,13 @@ describe("loop.OTSdkDriver", function () {
sinon.assert.calledOnce(session.subscribe);
sinon.assert.calledWithExactly(session.subscribe,
fakeStream, fakeScreenElement, publisherConfig);
fakeStream, sinon.match.instanceOf(HTMLDivElement), publisherConfig,
sinon.match.func);
});
it("should dispatch a mediaConnected action if both streams are up", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
driver._publishedLocalStream = true;
session.trigger("streamCreated", { stream: fakeStream });
@ -894,6 +936,8 @@ describe("loop.OTSdkDriver", function () {
it("should store the start time when both streams are up and" +
" driver._sendTwoWayMediaTelemetry is true", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
driver._sendTwoWayMediaTelemetry = true;
driver._publishedLocalStream = true;
var startTime = 1;
@ -906,6 +950,8 @@ describe("loop.OTSdkDriver", function () {
it("should not store the start time when both streams are up and" +
" driver._isDesktop is false", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement);
driver._isDesktop = false;
driver._publishedLocalStream = true;
var startTime = 73;
@ -936,8 +982,10 @@ describe("loop.OTSdkDriver", function () {
new sharedActions.ReceivingScreenShare({receiving: true}));
});
it("should dispatch a ReceivingScreenShare action for screen sharing streams",
function() {
// XXX See bug 1171933 and the comment in
// OtSdkDriver#_handleRemoteScreenShareCreated
it.skip("should dispatch a ReceivingScreenShare action for screen" +
" sharing streams", function() {
fakeStream.videoType = "screen";
session.trigger("streamCreated", { stream: fakeStream });
@ -949,7 +997,7 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("streamDestroyed (publisher/local)", function() {
describe("streamDestroyed: publisher/local", function() {
it("should dispatch a ConnectionStatus action", function() {
driver._metrics.sendStreams = 1;
driver._metrics.recvStreams = 1;
@ -969,7 +1017,7 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("streamDestroyed (session/remote)", function() {
describe("streamDestroyed: session/remote", function() {
var fakeStream;
beforeEach(function() {
@ -1182,6 +1230,36 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("videoEnabled", function() {
it("should dispatch RemoteVideoEnabled", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement).returns(this.fakeSubscriberObject);
session.trigger("streamCreated", {stream: fakeSubscriberObject.stream});
driver._mockSubscribeEl.appendChild(videoElement);
fakeSubscriberObject.trigger("videoEnabled");
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWith(dispatcher.dispatch,
new sharedActions.RemoteVideoEnabled({srcVideoObject: videoElement}));
});
});
describe("videoDisabled", function() {
it("should dispatch RemoteVideoDisabled", function() {
session.subscribe.yieldsOn(driver, null, fakeSubscriberObject,
videoElement).returns(this.fakeSubscriberObject);
session.trigger("streamCreated", {stream: fakeSubscriberObject.stream});
fakeSubscriberObject.trigger("videoDisabled");
sinon.assert.called(dispatcher.dispatch);
sinon.assert.calledWithExactly(dispatcher.dispatch,
new sharedActions.RemoteVideoDisabled({}));
});
});
describe("signal:readyForDataChannel", function() {
beforeEach(function() {
driver.subscriber = subscriber;
@ -1270,15 +1348,19 @@ describe("loop.OTSdkDriver", function () {
});
});
describe("Events (screenshare)", function() {
describe("Events: screenshare:", function() {
var videoElement;
beforeEach(function() {
driver.connectSession(sessionData);
driver.getScreenShareElementFunc = function() {};
driver.startScreenShare({
videoSource: "window"
});
// use a real video element so that these tests correctly reflect
// code behavior when run in whatever browser
videoElement = document.createElement("video");
});
describe("accessAllowed", function() {

View File

@ -814,5 +814,125 @@ describe("loop.shared.views", function() {
});
});
});
});
describe("MediaView", function() {
var view;
function mountTestComponent(props) {
return TestUtils.renderIntoDocument(
React.createElement(sharedViews.MediaView, props));
}
it("should display an avatar view", function() {
view = mountTestComponent({
displayAvatar: true,
mediaType: "local"
});
TestUtils.findRenderedComponentWithType(view,
sharedViews.AvatarView);
});
it("should display a no-video div if no source object is supplied", function() {
view = mountTestComponent({
displayAvatar: false,
mediaType: "local"
});
var element = view.getDOMNode();
expect(element.className).eql("no-video");
});
it("should display a video element if a source object is supplied", function() {
view = mountTestComponent({
displayAvatar: false,
mediaType: "local",
// This doesn't actually get assigned to the video element, but is enough
// for this test to check display of the video element.
srcVideoObject: {
fake: 1
}
});
var element = view.getDOMNode();
expect(element).not.eql(null);
expect(element.className).eql("local-video");
expect(element.muted).eql(true);
});
// We test this function by itself, as otherwise we'd be into creating fake
// streams etc.
describe("#attachVideo", function() {
var fakeViewElement;
beforeEach(function() {
fakeViewElement = {
play: sinon.stub(),
tagName: "VIDEO"
};
view = mountTestComponent({
displayAvatar: false,
mediaType: "local",
srcVideoObject: {
fake: 1
}
});
});
it("should not throw if no source object is specified", function() {
expect(function() {
view.attachVideo(null);
}).to.not.Throw();
});
it("should not throw if the element is not a video object", function() {
sinon.stub(view, "getDOMNode").returns({
tagName: "DIV"
});
expect(function() {
view.attachVideo({});
}).to.not.Throw();
});
it("should attach a video object according to the standard", function() {
fakeViewElement.srcObject = null;
sinon.stub(view, "getDOMNode").returns(fakeViewElement);
view.attachVideo({
srcObject: {fake: 1}
});
expect(fakeViewElement.srcObject).eql({fake: 1});
});
it("should attach a video object for Firefox", function() {
fakeViewElement.mozSrcObject = null;
sinon.stub(view, "getDOMNode").returns(fakeViewElement);
view.attachVideo({
mozSrcObject: {fake: 2}
});
expect(fakeViewElement.mozSrcObject).eql({fake: 2});
});
it("should attach a video object for Chrome", function() {
fakeViewElement.src = null;
sinon.stub(view, "getDOMNode").returns(fakeViewElement);
view.attachVideo({
src: {fake: 2}
});
expect(fakeViewElement.src).eql({fake: 2});
});
});
});
});

View File

@ -188,14 +188,6 @@ describe("loop.standaloneRoomViews", function() {
sinon.assert.calledOnce(dispatch);
sinon.assert.calledWithExactly(dispatch,
sinon.match.instanceOf(sharedActions.SetupStreamElements));
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
return value.getLocalElementFunc() ===
view.getDOMNode().querySelector(".local");
}));
sinon.assert.calledWithExactly(dispatch, sinon.match(function(value) {
return value.getRemoteElementFunc() ===
view.getDOMNode().querySelector(".remote");
}));
}
describe("#componentWillUpdate", function() {
@ -298,6 +290,10 @@ describe("loop.standaloneRoomViews", function() {
sandbox.stub(window, "matchMedia").returns({
matches: false
});
activeRoomStore.setStoreState({
remoteSrcVideoObject: {},
remoteVideoEnabled: true
});
view = mountTestComponent();
localElement = view._getElement(".local");
});
@ -317,6 +313,34 @@ describe("loop.standaloneRoomViews", function() {
expect(localElement.style.height).eql("120px");
});
it("should be a quarter of the width of the remote view element when there is no stream", function() {
activeRoomStore.setStoreState({
remoteSrcVideoObject: null,
remoteVideoEnabled: false
});
sandbox.stub(view, "getDOMNode").returns({
querySelector: function(selector) {
if (selector === ".local") {
return localElement;
}
return {
offsetWidth: 640,
offsetLeft: 0
};
}
});
view.updateLocalCameraPosition({
width: 1,
height: 0.75
});
expect(localElement.style.width).eql("160px");
expect(localElement.style.height).eql("120px");
});
it("should be a quarter of the width reduced for aspect ratio", function() {
sandbox.stub(view, "getRemoteVideoDimensions").returns({
streamWidth: 640,
@ -377,6 +401,34 @@ describe("loop.standaloneRoomViews", function() {
expect(localElement.style.left).eql("600px");
});
it("should position the stream to overlap the remote view element when there is no stream", function() {
activeRoomStore.setStoreState({
remoteSrcVideoObject: null,
remoteVideoEnabled: false
});
sandbox.stub(view, "getDOMNode").returns({
querySelector: function(selector) {
if (selector === ".local") {
return localElement;
}
return {
offsetWidth: 640,
offsetLeft: 0
};
}
});
view.updateLocalCameraPosition({
width: 1,
height: 0.75
});
expect(localElement.style.width).eql("160px");
expect(localElement.style.left).eql("600px");
});
it("should position the stream to overlap the main stream by a quarter when the aspect ratio is vertical", function() {
sandbox.stub(view, "getRemoteVideoDimensions").returns({
streamWidth: 640,
@ -576,6 +628,101 @@ describe("loop.standaloneRoomViews", function() {
});
});
describe("Participants", function() {
var videoElement;
beforeEach(function() {
videoElement = document.createElement("video");
});
it("should render local video when video_muted is false", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
localSrcVideoObject: videoElement,
videoMuted: false
});
expect(view.getDOMNode().querySelector(".local video")).not.eql(null);
});
it("should not render a local avatar when video_muted is false", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: false
});
expect(view.getDOMNode().querySelector(".local .avatar")).eql(null);
});
it("should render remote video when the room HAS_PARTICIPANTS and" +
" remoteVideoEnabled is true", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: videoElement,
remoteVideoEnabled: true
});
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
});
it("should not render remote video when the room HAS_PARTICIPANTS," +
" remoteVideoEnabled is false, and mediaConnected is true", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: videoElement,
mediaConnected: true,
remoteVideoEnabled: false
});
expect(view.getDOMNode().querySelector(".remote video")).eql(null);
});
it("should render remote video when the room HAS_PARTICIPANTS," +
" and both remoteVideoEnabled and mediaConnected are false", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: videoElement,
mediaConnected: false,
remoteVideoEnabled: false
});
expect(view.getDOMNode().querySelector(".remote video")).not.eql(null);
});
it("should not render a remote avatar when the room is in MEDIA_WAIT", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.MEDIA_WAIT,
remoteSrcVideoObject: videoElement,
remoteVideoEnabled: false
});
expect(view.getDOMNode().querySelector(".remote .avatar")).eql(null);
});
it("should not render a remote avatar when the room is CLOSING and" +
" remoteVideoEnabled is false", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.CLOSING,
remoteSrcVideoObject: videoElement,
remoteVideoEnabled: false
});
expect(view.getDOMNode().querySelector(".remote .avatar")).eql(null);
});
it("should render a remote avatar when the room HAS_PARTICIPANTS, " +
"remoteVideoEnabled is false, and mediaConnected is true", function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteSrcVideoObject: videoElement,
remoteVideoEnabled: false,
mediaConnected: true
});
expect(view.getDOMNode().querySelector(".remote .avatar")).not.eql(null);
});
});
describe("Leave button", function() {
function getLeaveButton(view) {
return view.getDOMNode().querySelector(".btn-hangup");
@ -676,6 +823,18 @@ describe("loop.standaloneRoomViews", function() {
expect(view.getDOMNode().querySelector(".local-stream-audio"))
.not.eql(null);
});
it("should render a local avatar if the room HAS_PARTICIPANTS and" +
" .videoMuted is true",
function() {
activeRoomStore.setStoreState({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
});
expect(view.getDOMNode().querySelector(".local .avatar")).
not.eql(null);
});
});
describe("Marketplace hidden iframe", function() {

View File

@ -84,6 +84,7 @@
</script>
<script src="../content/js/panel.js"></script>
<script src="../content/js/conversation.js"></script>
<script src="react-frame-component.js"></script>
<script src="ui-showcase.js"></script>
</body>
</html>

View File

@ -0,0 +1,138 @@
/*
* Copied from <https://github.com/ryanseddon/react-frame-component> 0.3.2,
* by Ryan Seddon, under the MIT license, since that original version requires
* a browserify-style loader.
*/
/**
* This is an array of frames that are queued and waiting to be loaded before
* their rendering is completed.
*
* @type {Array}
*/
window.queuedFrames = [];
/**
* Renders this.props.children inside an <iframe>.
*
* Works by creating the iframe, waiting for that to finish, and then
* rendering the children inside that. Waits for a while in the hopes that the
* contents will have been rendered, and then fires a callback indicating that.
*
* @see onContentsRendered for the gory details about this.
*
* @type {ReactComponentFactory<P>}
*/
window.Frame = React.createClass({
propTypes: {
style: React.PropTypes.object,
head: React.PropTypes.node,
width: React.PropTypes.number,
height: React.PropTypes.number,
onContentsRendered: React.PropTypes.func
},
render: function() {
return React.createElement("iframe", {
style: this.props.style,
head: this.props.head,
width: this.props.width,
height: this.props.height
});
},
componentDidMount: function() {
this.renderFrameContents();
},
renderFrameContents: function() {
var doc = this.getDOMNode().contentDocument;
if (doc && doc.readyState === "complete") {
// Remove this from the queue.
window.queuedFrames.splice(window.queuedFrames.indexOf(this), 1);
var iframeHead = doc.querySelector("head");
var parentHeadChildren = document.querySelector("head").children;
[].forEach.call(parentHeadChildren, function(parentHeadNode) {
iframeHead.appendChild(parentHeadNode.cloneNode(true));
});
var contents = React.createElement("div",
undefined,
this.props.head,
this.props.children
);
React.render(contents, doc.body, this.fireOnContentsRendered.bind(this));
// Set the RTL mode. We assume for now that rtl is the only query parameter.
//
// See also "ShowCase" in ui-showcase.jsx
if (document.location.search === "?rtl=1") {
doc.documentElement.setAttribute("lang", "ar");
doc.documentElement.setAttribute("dir", "rtl");
}
} else {
// Queue it, only if it isn't already. We do need to set the timeout
// regardless, as this function can get re-entered several times.
if (window.queuedFrames.indexOf(this) === -1) {
window.queuedFrames.push(this);
}
setTimeout(this.renderFrameContents.bind(this), 0);
}
},
/**
* Fires the onContentsRendered callback passed in via this.props,
* with the first argument set to the window global used by the iframe.
* This is useful in extracting things specific to that iframe (such as
* the matchMedia function) for use by code running in that iframe. Once
* React gets a more complete "context" feature:
*
* 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 avoid reaching into the DOM like this.
*
* XXX wait a little while. After React has rendered this iframe (eg the
* virtual DOM cache gets flushed to the browser), there's still more stuff
* that needs to happen before layout completes. If onContentsRendered fires
* before that happens, the wrong sizes (eg remote stream vertical height
* of 0) are used to compute the position in the MediaSetupStream, resulting
* in everything looking wonky. One high likelihood candidate for the delay
* here involves loading/decode poster images, but even using link
* rel=prefetch on those isn't enough to workaround this problem, so there
* may be more.
*
* There doesn't appear to be a good cross-browser way to handle this
* at the moment without gross violation of encapsulation (see
* http://stackoverflow.com/questions/27241186/how-to-determine-when-document-has-loaded-after-loading-external-csshttp://stackoverflow.com/questions/27241186/how-to-determine-when-document-has-loaded-after-loading-external-css
* for discussion of a related problem.
*
* For now, just wait for multiple seconds. Yuck.
*/
fireOnContentsRendered: function() {
if (!this.props.onContentsRendered) {
return;
}
var contentWindow;
try {
contentWindow = this.getDOMNode().contentWindow;
if (!contentWindow) {
throw new Error("no content window returned");
}
} catch (ex) {
console.error("exception getting content window", ex);
}
// Using bind to construct a "partial function", where |this| is unchanged,
// but the first parameter is guaranteed to be set. Details at
// https://developer.mozilla.org/de/docs/Web/JavaScript/Reference/Global_Objects/Function/bind#Example.3A_Partial_Functions
setTimeout(this.props.onContentsRendered.bind(undefined, contentWindow),
3000);
},
componentDidUpdate: function() {
this.renderFrameContents();
},
componentWillUnmount: function() {
React.unmountComponentAtNode(React.findDOMNode(this).contentDocument.body);
}
});

Binary file not shown.

Before

Width:  |  Height:  |  Size: 100 KiB

After

Width:  |  Height:  |  Size: 402 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 2.0 MiB

After

Width:  |  Height:  |  Size: 536 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 263 KiB

View File

@ -3,7 +3,9 @@
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
body {
/* Override the hidden in common.css */
/* Override the hidden in common.css. Very important otherwise you can't
* scroll the showcase.
*/
overflow: visible;
}
@ -84,6 +86,14 @@ body {
color: #555;
}
.showcase .checkbox-wrapper label {
font-weight: bold;
}
.showcase .checkbox.checked {
background-image: url("../content/shared/img/check.svg#check-blue");
}
.showcase p.note {
margin: 0;
padding: 0;
@ -107,72 +117,12 @@ body {
font-weight: bold;
}
/*
* Switched to using height: 100% in standalone version
* this mocks it for the ui so that the component has height
* */
.standalone .video-layout-wrapper,
.standalone .remote_wrapper {
min-height: 550px;
}
@media screen and (max-width:640px) {
.standalone .local-stream {
background-size: cover;
}
.standalone .local-stream,
.conversation .media.nested .remote {
background-size: cover;
background-position: center;
}
.standalone .remote_wrapper {
width: 100%;
background-size: cover;
background-position: center;
}
}
.remote_wrapper {
background-image: url("sample-img/video-screen-remote.png");
background-repeat: no-repeat;
background-size: cover;
}
.local-stream {
background-image: url("sample-img/video-screen-local.png");
background-repeat: no-repeat;
}
.local-stream.local:not(.local-stream-audio) {
background-size: cover;
}
.call-action-group .btn-group-chevron,
.call-action-group .btn-group {
/* Prevent box overflow due to long string */
max-width: 120px;
}
.conversation .media.nested .remote {
/* Height of obsolute box covers media control buttons. UI showcase only.
* When tokbox inserts the markup into the page the problem goes away */
bottom: auto;
}
.standalone .ended-conversation .remote_wrapper,
.standalone .video-layout-wrapper {
/* Removes the fake video image for ended conversations */
background: none;
}
/* Rooms edge cases */
.standalone .room-conversation .remote_wrapper {
background: none;
}
/* SVG icons showcase */
.svg-icons h3 {

View File

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global uncaughtError:true */
/* global Frame:false uncaughtError:true */
(function() {
"use strict";
@ -18,6 +18,7 @@
// 1.2. Conversation Window
var AcceptCallView = loop.conversationViews.AcceptCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var OngoingConversationView = loop.conversationViews.OngoingConversationView;
var CallFailedView = loop.conversationViews.CallFailedView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
@ -25,18 +26,12 @@
var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var GumPromptConversationView = loop.webapp.GumPromptConversationView;
var WaitingConversationView = loop.webapp.WaitingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var ConversationView = loop.shared.views.ConversationView;
var FeedbackView = loop.shared.views.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
@ -94,13 +89,154 @@
}
}, Backbone.Events);
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
/**
* Every view that uses an activeRoomStore needs its own; if they shared
* an active store, they'd interfere with each other.
*
* @param options
* @returns {loop.store.ActiveRoomStore}
*/
function makeActiveRoomStore(options) {
var dispatcher = new loop.Dispatcher();
var store = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
if (!("remoteVideoEnabled" in options)) {
options.remoteVideoEnabled = true;
}
if (!("mediaConnected" in options)) {
options.mediaConnected = true;
}
store.setStoreState({
mediaConnected: options.mediaConnected,
remoteVideoEnabled: options.remoteVideoEnabled,
roomName: "A Very Long Conversation Name",
roomState: options.roomState,
used: !!options.roomUsed,
videoMuted: !!options.videoMuted
});
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 {
// the dimensions here are taken from the poster images that we're
// using, since they give the <video> elements their initial intrinsic
// size. This ensures that the right aspect ratios are calculated.
// These are forced to 640x480, because it makes it visually easy to
// validate that the showcase looks like the real app on a chine
// (eg MacBook Pro) where that is the default camera resolution.
var newStoreState = {
localVideoDimensions: {
camera: {height: 480, orientation: 0, width: 640}
},
mediaConnected: options.mediaConnected,
receivingScreenShare: !!options.receivingScreenShare,
remoteVideoDimensions: {
camera: {height: 480, orientation: 0, width: 640}
},
remoteVideoEnabled: options.remoteVideoEnabled,
matchMedia: contentWindow.matchMedia.bind(contentWindow),
roomState: options.roomState,
videoMuted: !!options.videoMuted
};
if (options.receivingScreenShare) {
// Note that the image we're using had to be scaled a bit, and
// it still ended up a bit narrower than the live thing that
// WebRTC sends; presumably a different scaling algorithm.
// For showcase purposes, this shouldn't matter much, as the sizes
// of things being shared will be fairly arbitrary.
newStoreState.remoteVideoDimensions.screen =
{height: 456, orientation: 0, width: 641};
}
store.setStoreState(newStoreState);
} catch (ex) {
console.error("exception in forcedUpdate:", ex);
}
};
return store;
}
var activeRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var joinedRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.JOINED,
remoteVideoEnabled: false
});
var readyRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.READY
});
var updatingActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var localFaceMuteRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
});
var remoteFaceMuteRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteVideoEnabled: false,
mediaConnected: true
});
var updatingSharingRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
receivingScreenShare: true
});
var fullActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.FULL
});
var failedRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.FAILED
});
var endedRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.ENDED,
roomUsed: true
});
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
});
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
});
var desktopLocalFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: desktopLocalFaceMuteActiveRoomStore
});
var desktopRemoteFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteVideoEnabled: false,
mediaConnected: true
});
var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: stageFeedbackApiClient
});
@ -216,6 +352,37 @@
}
});
var FramedExample = React.createClass({displayName: "FramedExample",
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
onContentsRendered: React.PropTypes.func
},
makeId: function(prefix) {
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
},
render: function() {
var cx = React.addons.classSet;
return (
React.createElement("div", {className: "example"},
React.createElement("h3", {id: this.makeId()},
this.props.summary,
React.createElement("a", {href: this.makeId("#")}, " ¶")
),
React.createElement("div", {className: cx({comp: true, dashed: this.props.dashed}),
style: this.props.style},
React.createElement(Frame, {width: this.props.width, height: this.props.height,
onContentsRendered: this.props.onContentsRendered},
this.props.children
)
)
)
);
}
});
var Example = React.createClass({displayName: "Example",
makeId: function(prefix) {
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
@ -250,11 +417,42 @@
});
var ShowCase = React.createClass({displayName: "ShowCase",
getInitialState: function() {
// We assume for now that rtl is the only query parameter.
//
// Note: this check is repeated in react-frame-component to save passing
// rtlMode down the props tree.
var rtlMode = document.location.search === "?rtl=1";
return {
rtlMode: rtlMode
};
},
_handleCheckboxChange: function(newState) {
var newLocation = "";
if (newState.checked) {
newLocation = document.location.href.split("#")[0];
newLocation += "?rtl=1";
} else {
newLocation = document.location.href.split("?")[0];
}
newLocation += document.location.hash;
document.location = newLocation;
},
render: function() {
if (this.state.rtlMode) {
document.documentElement.setAttribute("lang", "ar");
document.documentElement.setAttribute("dir", "rtl");
}
return (
React.createElement("div", {className: "showcase"},
React.createElement("header", null,
React.createElement("h1", null, "Loop UI Components Showcase"),
React.createElement(Checkbox, {label: "RTL mode?", checked: this.state.rtlMode,
onChange: this._handleCheckboxChange}),
React.createElement("nav", {className: "showcase-menu"},
React.Children.map(this.props.children, function(section) {
return (
@ -272,6 +470,7 @@
});
var App = React.createClass({displayName: "App",
render: function() {
return (
React.createElement(ShowCase, null,
@ -364,19 +563,19 @@
React.createElement(Section, {name: "ConversationToolbar"},
React.createElement("h2", null, "Desktop Conversation Window"),
React.createElement("div", {className: "fx-embedded override-position"},
React.createElement(Example, {summary: "Default", dashed: "true", style: {width: "300px", height: "272px"}},
React.createElement(Example, {summary: "Default", style: {width: "300px", height: "26px"}},
React.createElement(ConversationToolbar, {video: {enabled: true},
audio: {enabled: true},
hangup: noop,
publishStream: noop})
),
React.createElement(Example, {summary: "Video muted", style: {width: "300px", height: "272px"}},
React.createElement(Example, {summary: "Video muted", style: {width: "300px", height: "26px"}},
React.createElement(ConversationToolbar, {video: {enabled: false},
audio: {enabled: true},
hangup: noop,
publishStream: noop})
),
React.createElement(Example, {summary: "Audio muted", style: {width: "300px", height: "272px"}},
React.createElement(Example, {summary: "Audio muted", style: {width: "300px", height: "26px"}},
React.createElement(ConversationToolbar, {video: {enabled: true},
audio: {enabled: false},
hangup: noop,
@ -407,30 +606,6 @@
)
),
React.createElement(Section, {name: "GumPromptConversationView"},
React.createElement(Example, {summary: "Gum Prompt conversation view", dashed: "true"},
React.createElement("div", {className: "standalone"},
React.createElement(GumPromptConversationView, null)
)
)
),
React.createElement(Section, {name: "WaitingConversationView"},
React.createElement(Example, {summary: "Waiting conversation view (connecting)", dashed: "true"},
React.createElement("div", {className: "standalone"},
React.createElement(WaitingConversationView, {websocket: mockWebSocket,
dispatcher: dispatcher})
)
),
React.createElement(Example, {summary: "Waiting conversation view (ringing)", dashed: "true"},
React.createElement("div", {className: "standalone"},
React.createElement(WaitingConversationView, {websocket: mockWebSocket,
dispatcher: dispatcher,
callState: "ringing"})
)
)
),
React.createElement(Section, {name: "PendingConversationView (Desktop)"},
React.createElement(Example, {summary: "Connecting", dashed: "true",
style: {width: "300px", height: "272px"}},
@ -469,94 +644,61 @@
)
),
React.createElement(Section, {name: "StartConversationView"},
React.createElement(Example, {summary: "Start conversation view", dashed: "true"},
React.createElement("div", {className: "standalone"},
React.createElement(StartConversationView, {conversation: mockConversationModel,
client: mockClient,
notifications: notifications})
)
)
),
React.createElement(Section, {name: "FailedConversationView"},
React.createElement(Example, {summary: "Failed conversation view", dashed: "true"},
React.createElement("div", {className: "standalone"},
React.createElement(FailedConversationView, {conversation: mockConversationModel,
client: mockClient,
notifications: notifications})
)
)
),
React.createElement(Section, {name: "ConversationView"},
React.createElement(Example, {summary: "Desktop conversation window", dashed: "true",
style: {width: "300px", height: "272px"}},
React.createElement(Section, {name: "OngoingConversationView"},
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop ongoing conversation window"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(ConversationView, {sdk: mockSDK,
model: mockConversationModel,
video: {enabled: true},
audio: {enabled: true}})
)
),
React.createElement(Example, {summary: "Desktop conversation window large", dashed: "true"},
React.createElement("div", {className: "breakpoint", "data-breakpoint-width": "800px",
"data-breakpoint-height": "600px"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(ConversationView, {sdk: mockSDK,
React.createElement(OngoingConversationView, {
dispatcher: dispatcher,
video: {enabled: true},
audio: {enabled: true},
model: mockConversationModel})
)
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
mediaConnected: true})
)
),
React.createElement(Example, {summary: "Desktop conversation window local audio stream",
dashed: "true", style: {width: "300px", height: "272px"}},
React.createElement(FramedExample, {width: 800, height: 600,
summary: "Desktop ongoing conversation window large"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(ConversationView, {sdk: mockSDK,
React.createElement(OngoingConversationView, {
dispatcher: dispatcher,
video: {enabled: true},
audio: {enabled: true},
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
remoteVideoEnabled: true,
mediaConnected: true})
)
),
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop ongoing conversation window - local face mute"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
dispatcher: dispatcher,
video: {enabled: false},
audio: {enabled: true},
model: mockConversationModel})
remoteVideoEnabled: true,
remotePosterUrl: "sample-img/video-screen-remote.png",
mediaConnected: true})
)
),
React.createElement(Example, {summary: "Standalone version"},
React.createElement("div", {className: "standalone"},
React.createElement(ConversationView, {sdk: mockSDK,
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop ongoing conversation window - remote face mute"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(OngoingConversationView, {
dispatcher: dispatcher,
video: {enabled: true},
audio: {enabled: true},
model: mockConversationModel})
remoteVideoEnabled: false,
localPosterUrl: "sample-img/video-screen-local.png",
mediaConnected: true})
)
)
),
React.createElement(Section, {name: "ConversationView-640"},
React.createElement(Example, {summary: "640px breakpoint for conversation view"},
React.createElement("div", {className: "breakpoint",
style: {"text-align": "center"},
"data-breakpoint-width": "400px",
"data-breakpoint-height": "780px"},
React.createElement("div", {className: "standalone"},
React.createElement(ConversationView, {sdk: mockSDK,
video: {enabled: true},
audio: {enabled: true},
model: mockConversationModel})
)
)
)
),
React.createElement(Section, {name: "ConversationView-LocalAudio"},
React.createElement(Example, {summary: "Local stream is audio only"},
React.createElement("div", {className: "standalone"},
React.createElement(ConversationView, {sdk: mockSDK,
video: {enabled: false},
audio: {enabled: true},
model: mockConversationModel})
)
)
),
React.createElement(Section, {name: "FeedbackView"},
@ -575,28 +717,6 @@
)
),
React.createElement(Section, {name: "CallUrlExpiredView"},
React.createElement(Example, {summary: "Firefox User"},
React.createElement(CallUrlExpiredView, {isFirefox: true})
),
React.createElement(Example, {summary: "Non-Firefox User"},
React.createElement(CallUrlExpiredView, {isFirefox: false})
)
),
React.createElement(Section, {name: "EndedConversationView"},
React.createElement(Example, {summary: "Displays the feedback form"},
React.createElement("div", {className: "standalone"},
React.createElement(EndedConversationView, {sdk: mockSDK,
video: {enabled: true},
audio: {enabled: true},
conversation: mockConversationModel,
feedbackStore: feedbackStore,
onAfterFeedbackReceived: noop})
)
)
),
React.createElement(Section, {name: "AlertMessages"},
React.createElement(Example, {summary: "Various alerts"},
React.createElement("div", {className: "alert alert-warning"},
@ -615,15 +735,6 @@
)
),
React.createElement(Section, {name: "HomeView"},
React.createElement(Example, {summary: "Standalone Home View"},
React.createElement("div", {className: "standalone"},
React.createElement(HomeView, null)
)
)
),
React.createElement(Section, {name: "UnsupportedBrowserView"},
React.createElement(Example, {summary: "Standalone Unsupported Browser"},
React.createElement("div", {className: "standalone"},
@ -641,97 +752,171 @@
),
React.createElement(Section, {name: "DesktopRoomConversationView"},
React.createElement(Example, {summary: "Desktop room conversation (invitation)", dashed: "true",
style: {width: "260px", height: "265px"}},
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop room conversation (invitation)"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
roomStore: roomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
localPosterUrl: "sample-img/video-screen-local.png",
roomState: ROOM_STATES.INIT})
)
),
React.createElement(Example, {summary: "Desktop room conversation", dashed: "true",
style: {width: "260px", height: "265px"}},
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop room conversation"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
roomStore: roomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
roomState: ROOM_STATES.HAS_PARTICIPANTS})
)
),
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop room conversation local face-mute"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
roomStore: desktopLocalFaceMuteRoomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
remotePosterUrl: "sample-img/video-screen-remote.png"})
)
),
React.createElement(FramedExample, {width: 298, height: 254,
summary: "Desktop room conversation remote face-mute"},
React.createElement("div", {className: "fx-embedded"},
React.createElement(DesktopRoomConversationView, {
roomStore: desktopRemoteFaceMuteRoomStore,
dispatcher: dispatcher,
mozLoop: navigator.mozLoop,
localPosterUrl: "sample-img/video-screen-local.png"})
)
)
),
React.createElement(Section, {name: "StandaloneRoomView"},
React.createElement(Example, {summary: "Standalone room conversation (ready)"},
React.createElement(FramedExample, {width: 644, height: 483,
summary: "Standalone room conversation (ready)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
activeRoomStore: readyRoomStore,
roomState: ROOM_STATES.READY,
isFirefox: true})
)
),
React.createElement(Example, {summary: "Standalone room conversation (joined)"},
React.createElement(FramedExample, {width: 644, height: 483,
summary: "Standalone room conversation (joined)",
onContentsRendered: joinedRoomStore.forcedUpdate},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.JOINED,
activeRoomStore: joinedRoomStore,
localPosterUrl: "sample-img/video-screen-local.png",
isFirefox: true})
)
),
React.createElement(Example, {summary: "Standalone room conversation (has-participants)"},
React.createElement(FramedExample, {width: 644, height: 483,
onContentsRendered: updatingActiveRoomStore.forcedUpdate,
summary: "Standalone room conversation (has-participants, 644x483)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
activeRoomStore: updatingActiveRoomStore,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png"})
)
),
React.createElement(FramedExample, {width: 644, height: 483,
onContentsRendered: localFaceMuteRoomStore.forcedUpdate,
summary: "Standalone room conversation (local face mute, has-participants, 644x483)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: localFaceMuteRoomStore,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png"})
)
),
React.createElement(FramedExample, {width: 644, height: 483,
onContentsRendered: remoteFaceMuteRoomStore.forcedUpdate,
summary: "Standalone room conversation (remote face mute, has-participants, 644x483)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: remoteFaceMuteRoomStore,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png"})
)
),
React.createElement(FramedExample, {width: 800, height: 660,
onContentsRendered: updatingSharingRoomStore.forcedUpdate,
summary: "Standalone room convo (has-participants, receivingScreenShare, 800x660)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: updatingSharingRoomStore,
roomState: ROOM_STATES.HAS_PARTICIPANTS,
isFirefox: true,
localPosterUrl: "sample-img/video-screen-local.png",
remotePosterUrl: "sample-img/video-screen-remote.png",
screenSharePosterUrl: "sample-img/video-screen-terminal.png"}
)
)
),
React.createElement(FramedExample, {width: 644, height: 483,
summary: "Standalone room conversation (full - FFx user)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: fullActiveRoomStore,
isFirefox: true})
)
),
React.createElement(Example, {summary: "Standalone room conversation (full - FFx user)"},
React.createElement(FramedExample, {width: 644, height: 483,
summary: "Standalone room conversation (full - non FFx user)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.FULL,
isFirefox: true})
)
),
React.createElement(Example, {summary: "Standalone room conversation (full - non FFx user)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.FULL,
activeRoomStore: fullActiveRoomStore,
isFirefox: false})
)
),
React.createElement(Example, {summary: "Standalone room conversation (feedback)"},
React.createElement(FramedExample, {width: 644, height: 483,
summary: "Standalone room conversation (feedback)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
activeRoomStore: endedRoomStore,
feedbackStore: feedbackStore,
roomState: ROOM_STATES.ENDED,
isFirefox: false})
)
),
React.createElement(Example, {summary: "Standalone room conversation (failed)"},
React.createElement(FramedExample, {width: 644, height: 483,
summary: "Standalone room conversation (failed)"},
React.createElement("div", {className: "standalone"},
React.createElement(StandaloneRoomView, {
dispatcher: dispatcher,
activeRoomStore: activeRoomStore,
roomState: ROOM_STATES.FAILED,
activeRoomStore: failedRoomStore,
isFirefox: false})
)
)
@ -754,45 +939,6 @@
}
});
/**
* Render components that have different styles across
* CSS media rules in their own iframe to mimic the viewport
* */
function _renderComponentsInIframes() {
var parents = document.querySelectorAll(".breakpoint");
[].forEach.call(parents, appendChildInIframe);
/**
* Extracts the component from the DOM and appends in the an iframe
*
* @type {HTMLElement} parent - Parent DOM node of a component & iframe
* */
function appendChildInIframe(parent) {
var styles = document.querySelector("head").children;
var component = parent.children[0];
var iframe = document.createElement("iframe");
var width = parent.dataset.breakpointWidth;
var height = parent.dataset.breakpointHeight;
iframe.style.width = width;
iframe.style.height = height;
parent.appendChild(iframe);
iframe.src = "about:blank";
// Workaround for bug 297685
iframe.onload = function () {
var iframeHead = iframe.contentDocument.querySelector("head");
iframe.contentDocument.documentElement.querySelector("body")
.appendChild(component);
[].forEach.call(styles, function(style) {
iframeHead.appendChild(style.cloneNode(true));
});
};
}
}
window.addEventListener("DOMContentLoaded", function() {
try {
React.renderComponent(React.createElement(App, null), document.getElementById("main"));
@ -805,8 +951,12 @@
uncaughtError = err;
}
_renderComponentsInIframes();
// Wait until all the FramedExamples have been fully loaded.
setTimeout(function waitForQueuedFrames() {
if (window.queuedFrames.length != 0) {
setTimeout(waitForQueuedFrames, 500);
return;
}
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
@ -822,6 +972,7 @@
$("#results").append("<div class='failures'><em>0</em></div>");
}
$("#results").append("<p id='complete'>Complete.</p>");
}, 1000);
});
})();

View File

@ -2,7 +2,7 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
/* global uncaughtError:true */
/* global Frame:false uncaughtError:true */
(function() {
"use strict";
@ -18,6 +18,7 @@
// 1.2. Conversation Window
var AcceptCallView = loop.conversationViews.AcceptCallView;
var DesktopPendingConversationView = loop.conversationViews.PendingConversationView;
var OngoingConversationView = loop.conversationViews.OngoingConversationView;
var CallFailedView = loop.conversationViews.CallFailedView;
var DesktopRoomConversationView = loop.roomViews.DesktopRoomConversationView;
@ -25,18 +26,12 @@
var HomeView = loop.webapp.HomeView;
var UnsupportedBrowserView = loop.webapp.UnsupportedBrowserView;
var UnsupportedDeviceView = loop.webapp.UnsupportedDeviceView;
var CallUrlExpiredView = loop.webapp.CallUrlExpiredView;
var GumPromptConversationView = loop.webapp.GumPromptConversationView;
var WaitingConversationView = loop.webapp.WaitingConversationView;
var StartConversationView = loop.webapp.StartConversationView;
var FailedConversationView = loop.webapp.FailedConversationView;
var EndedConversationView = loop.webapp.EndedConversationView;
var StandaloneRoomView = loop.standaloneRoomViews.StandaloneRoomView;
// 3. Shared components
var ConversationToolbar = loop.shared.views.ConversationToolbar;
var ConversationView = loop.shared.views.ConversationView;
var FeedbackView = loop.shared.views.FeedbackView;
var Checkbox = loop.shared.views.Checkbox;
// Store constants
var ROOM_STATES = loop.store.ROOM_STATES;
@ -94,13 +89,154 @@
}
}, Backbone.Events);
var activeRoomStore = new loop.store.ActiveRoomStore(dispatcher, {
/**
* Every view that uses an activeRoomStore needs its own; if they shared
* an active store, they'd interfere with each other.
*
* @param options
* @returns {loop.store.ActiveRoomStore}
*/
function makeActiveRoomStore(options) {
var dispatcher = new loop.Dispatcher();
var store = new loop.store.ActiveRoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
sdkDriver: mockSDK
});
if (!("remoteVideoEnabled" in options)) {
options.remoteVideoEnabled = true;
}
if (!("mediaConnected" in options)) {
options.mediaConnected = true;
}
store.setStoreState({
mediaConnected: options.mediaConnected,
remoteVideoEnabled: options.remoteVideoEnabled,
roomName: "A Very Long Conversation Name",
roomState: options.roomState,
used: !!options.roomUsed,
videoMuted: !!options.videoMuted
});
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 {
// the dimensions here are taken from the poster images that we're
// using, since they give the <video> elements their initial intrinsic
// size. This ensures that the right aspect ratios are calculated.
// These are forced to 640x480, because it makes it visually easy to
// validate that the showcase looks like the real app on a chine
// (eg MacBook Pro) where that is the default camera resolution.
var newStoreState = {
localVideoDimensions: {
camera: {height: 480, orientation: 0, width: 640}
},
mediaConnected: options.mediaConnected,
receivingScreenShare: !!options.receivingScreenShare,
remoteVideoDimensions: {
camera: {height: 480, orientation: 0, width: 640}
},
remoteVideoEnabled: options.remoteVideoEnabled,
matchMedia: contentWindow.matchMedia.bind(contentWindow),
roomState: options.roomState,
videoMuted: !!options.videoMuted
};
if (options.receivingScreenShare) {
// Note that the image we're using had to be scaled a bit, and
// it still ended up a bit narrower than the live thing that
// WebRTC sends; presumably a different scaling algorithm.
// For showcase purposes, this shouldn't matter much, as the sizes
// of things being shared will be fairly arbitrary.
newStoreState.remoteVideoDimensions.screen =
{height: 456, orientation: 0, width: 641};
}
store.setStoreState(newStoreState);
} catch (ex) {
console.error("exception in forcedUpdate:", ex);
}
};
return store;
}
var activeRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var joinedRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.JOINED,
remoteVideoEnabled: false
});
var readyRoomStore = makeActiveRoomStore({
mediaConnected: false,
roomState: ROOM_STATES.READY
});
var updatingActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS
});
var localFaceMuteRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
});
var remoteFaceMuteRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteVideoEnabled: false,
mediaConnected: true
});
var updatingSharingRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
receivingScreenShare: true
});
var fullActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.FULL
});
var failedRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.FAILED
});
var endedRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.ENDED,
roomUsed: true
});
var roomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop
});
var desktopLocalFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
videoMuted: true
});
var desktopLocalFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: desktopLocalFaceMuteActiveRoomStore
});
var desktopRemoteFaceMuteActiveRoomStore = makeActiveRoomStore({
roomState: ROOM_STATES.HAS_PARTICIPANTS,
remoteVideoEnabled: false,
mediaConnected: true
});
var desktopRemoteFaceMuteRoomStore = new loop.store.RoomStore(dispatcher, {
mozLoop: navigator.mozLoop,
activeRoomStore: desktopRemoteFaceMuteActiveRoomStore
});
var feedbackStore = new loop.store.FeedbackStore(dispatcher, {
feedbackClient: stageFeedbackApiClient
});
@ -216,6 +352,37 @@
}
});
var FramedExample = React.createClass({
propTypes: {
width: React.PropTypes.number,
height: React.PropTypes.number,
onContentsRendered: React.PropTypes.func
},
makeId: function(prefix) {
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
},
render: function() {
var cx = React.addons.classSet;
return (
<div className="example">
<h3 id={this.makeId()}>
{this.props.summary}
<a href={this.makeId("#")}>&nbsp;</a>
</h3>
<div className={cx({comp: true, dashed: this.props.dashed})}
style={this.props.style}>
<Frame width={this.props.width} height={this.props.height}
onContentsRendered={this.props.onContentsRendered}>
{this.props.children}
</Frame>
</div>
</div>
);
}
});
var Example = React.createClass({
makeId: function(prefix) {
return (prefix || "") + this.props.summary.toLowerCase().replace(/\s/g, "-");
@ -250,11 +417,42 @@
});
var ShowCase = React.createClass({
getInitialState: function() {
// We assume for now that rtl is the only query parameter.
//
// Note: this check is repeated in react-frame-component to save passing
// rtlMode down the props tree.
var rtlMode = document.location.search === "?rtl=1";
return {
rtlMode: rtlMode
};
},
_handleCheckboxChange: function(newState) {
var newLocation = "";
if (newState.checked) {
newLocation = document.location.href.split("#")[0];
newLocation += "?rtl=1";
} else {
newLocation = document.location.href.split("?")[0];
}
newLocation += document.location.hash;
document.location = newLocation;
},
render: function() {
if (this.state.rtlMode) {
document.documentElement.setAttribute("lang", "ar");
document.documentElement.setAttribute("dir", "rtl");
}
return (
<div className="showcase">
<header>
<h1>Loop UI Components Showcase</h1>
<Checkbox label="RTL mode?" checked={this.state.rtlMode}
onChange={this._handleCheckboxChange} />
<nav className="showcase-menu">{
React.Children.map(this.props.children, function(section) {
return (
@ -272,6 +470,7 @@
});
var App = React.createClass({
render: function() {
return (
<ShowCase>
@ -364,19 +563,19 @@
<Section name="ConversationToolbar">
<h2>Desktop Conversation Window</h2>
<div className="fx-embedded override-position">
<Example summary="Default" dashed="true" style={{width: "300px", height: "272px"}}>
<Example summary="Default" style={{width: "300px", height: "26px"}}>
<ConversationToolbar video={{enabled: true}}
audio={{enabled: true}}
hangup={noop}
publishStream={noop} />
</Example>
<Example summary="Video muted" style={{width: "300px", height: "272px"}}>
<Example summary="Video muted" style={{width: "300px", height: "26px"}}>
<ConversationToolbar video={{enabled: false}}
audio={{enabled: true}}
hangup={noop}
publishStream={noop} />
</Example>
<Example summary="Audio muted" style={{width: "300px", height: "272px"}}>
<Example summary="Audio muted" style={{width: "300px", height: "26px"}}>
<ConversationToolbar video={{enabled: true}}
audio={{enabled: false}}
hangup={noop}
@ -407,30 +606,6 @@
</div>
</Section>
<Section name="GumPromptConversationView">
<Example summary="Gum Prompt conversation view" dashed="true">
<div className="standalone">
<GumPromptConversationView />
</div>
</Example>
</Section>
<Section name="WaitingConversationView">
<Example summary="Waiting conversation view (connecting)" dashed="true">
<div className="standalone">
<WaitingConversationView websocket={mockWebSocket}
dispatcher={dispatcher} />
</div>
</Example>
<Example summary="Waiting conversation view (ringing)" dashed="true">
<div className="standalone">
<WaitingConversationView websocket={mockWebSocket}
dispatcher={dispatcher}
callState="ringing"/>
</div>
</Example>
</Section>
<Section name="PendingConversationView (Desktop)">
<Example summary="Connecting" dashed="true"
style={{width: "300px", height: "272px"}}>
@ -469,94 +644,61 @@
</Example>
</Section>
<Section name="StartConversationView">
<Example summary="Start conversation view" dashed="true">
<div className="standalone">
<StartConversationView conversation={mockConversationModel}
client={mockClient}
notifications={notifications} />
</div>
</Example>
</Section>
<Section name="FailedConversationView">
<Example summary="Failed conversation view" dashed="true">
<div className="standalone">
<FailedConversationView conversation={mockConversationModel}
client={mockClient}
notifications={notifications} />
</div>
</Example>
</Section>
<Section name="ConversationView">
<Example summary="Desktop conversation window" dashed="true"
style={{width: "300px", height: "272px"}}>
<Section name="OngoingConversationView">
<FramedExample width={298} height={254}
summary="Desktop ongoing conversation window">
<div className="fx-embedded">
<ConversationView sdk={mockSDK}
model={mockConversationModel}
video={{enabled: true}}
audio={{enabled: true}} />
</div>
</Example>
<Example summary="Desktop conversation window large" dashed="true">
<div className="breakpoint" data-breakpoint-width="800px"
data-breakpoint-height="600px">
<div className="fx-embedded">
<ConversationView sdk={mockSDK}
<OngoingConversationView
dispatcher={dispatcher}
video={{enabled: true}}
audio={{enabled: true}}
model={mockConversationModel} />
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
mediaConnected={true} />
</div>
</div>
</Example>
</FramedExample>
<Example summary="Desktop conversation window local audio stream"
dashed="true" style={{width: "300px", height: "272px"}}>
<FramedExample width={800} height={600}
summary="Desktop ongoing conversation window large">
<div className="fx-embedded">
<ConversationView sdk={mockSDK}
<OngoingConversationView
dispatcher={dispatcher}
video={{enabled: true}}
audio={{enabled: true}}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
remoteVideoEnabled={true}
mediaConnected={true} />
</div>
</FramedExample>
<FramedExample width={298} height={254}
summary="Desktop ongoing conversation window - local face mute">
<div className="fx-embedded">
<OngoingConversationView
dispatcher={dispatcher}
video={{enabled: false}}
audio={{enabled: true}}
model={mockConversationModel} />
remoteVideoEnabled={true}
remotePosterUrl="sample-img/video-screen-remote.png"
mediaConnected={true} />
</div>
</Example>
</FramedExample>
<Example summary="Standalone version">
<div className="standalone">
<ConversationView sdk={mockSDK}
<FramedExample width={298} height={254}
summary="Desktop ongoing conversation window - remote face mute">
<div className="fx-embedded">
<OngoingConversationView
dispatcher={dispatcher}
video={{enabled: true}}
audio={{enabled: true}}
model={mockConversationModel} />
remoteVideoEnabled={false}
localPosterUrl="sample-img/video-screen-local.png"
mediaConnected={true} />
</div>
</Example>
</Section>
</FramedExample>
<Section name="ConversationView-640">
<Example summary="640px breakpoint for conversation view">
<div className="breakpoint"
style={{"text-align": "center"}}
data-breakpoint-width="400px"
data-breakpoint-height="780px">
<div className="standalone">
<ConversationView sdk={mockSDK}
video={{enabled: true}}
audio={{enabled: true}}
model={mockConversationModel} />
</div>
</div>
</Example>
</Section>
<Section name="ConversationView-LocalAudio">
<Example summary="Local stream is audio only">
<div className="standalone">
<ConversationView sdk={mockSDK}
video={{enabled: false}}
audio={{enabled: true}}
model={mockConversationModel} />
</div>
</Example>
</Section>
<Section name="FeedbackView">
@ -575,28 +717,6 @@
</Example>
</Section>
<Section name="CallUrlExpiredView">
<Example summary="Firefox User">
<CallUrlExpiredView isFirefox={true} />
</Example>
<Example summary="Non-Firefox User">
<CallUrlExpiredView isFirefox={false} />
</Example>
</Section>
<Section name="EndedConversationView">
<Example summary="Displays the feedback form">
<div className="standalone">
<EndedConversationView sdk={mockSDK}
video={{enabled: true}}
audio={{enabled: true}}
conversation={mockConversationModel}
feedbackStore={feedbackStore}
onAfterFeedbackReceived={noop} />
</div>
</Example>
</Section>
<Section name="AlertMessages">
<Example summary="Various alerts">
<div className="alert alert-warning">
@ -615,15 +735,6 @@
</Example>
</Section>
<Section name="HomeView">
<Example summary="Standalone Home View">
<div className="standalone">
<HomeView />
</div>
</Example>
</Section>
<Section name="UnsupportedBrowserView">
<Example summary="Standalone Unsupported Browser">
<div className="standalone">
@ -641,100 +752,174 @@
</Section>
<Section name="DesktopRoomConversationView">
<Example summary="Desktop room conversation (invitation)" dashed="true"
style={{width: "260px", height: "265px"}}>
<FramedExample width={298} height={254}
summary="Desktop room conversation (invitation)">
<div className="fx-embedded">
<DesktopRoomConversationView
roomStore={roomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
localPosterUrl="sample-img/video-screen-local.png"
roomState={ROOM_STATES.INIT} />
</div>
</Example>
</FramedExample>
<Example summary="Desktop room conversation" dashed="true"
style={{width: "260px", height: "265px"}}>
<FramedExample width={298} height={254}
summary="Desktop room conversation">
<div className="fx-embedded">
<DesktopRoomConversationView
roomStore={roomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
roomState={ROOM_STATES.HAS_PARTICIPANTS} />
</div>
</Example>
</FramedExample>
<FramedExample width={298} height={254}
summary="Desktop room conversation local face-mute">
<div className="fx-embedded">
<DesktopRoomConversationView
roomStore={desktopLocalFaceMuteRoomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
remotePosterUrl="sample-img/video-screen-remote.png" />
</div>
</FramedExample>
<FramedExample width={298} height={254}
summary="Desktop room conversation remote face-mute">
<div className="fx-embedded">
<DesktopRoomConversationView
roomStore={desktopRemoteFaceMuteRoomStore}
dispatcher={dispatcher}
mozLoop={navigator.mozLoop}
localPosterUrl="sample-img/video-screen-local.png" />
</div>
</FramedExample>
</Section>
<Section name="StandaloneRoomView">
<Example summary="Standalone room conversation (ready)">
<FramedExample width={644} height={483}
summary="Standalone room conversation (ready)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
activeRoomStore={readyRoomStore}
roomState={ROOM_STATES.READY}
isFirefox={true} />
</div>
</Example>
</FramedExample>
<Example summary="Standalone room conversation (joined)">
<FramedExample width={644} height={483}
summary="Standalone room conversation (joined)"
onContentsRendered={joinedRoomStore.forcedUpdate}>
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.JOINED}
activeRoomStore={joinedRoomStore}
localPosterUrl="sample-img/video-screen-local.png"
isFirefox={true} />
</div>
</Example>
</FramedExample>
<Example summary="Standalone room conversation (has-participants)">
<FramedExample width={644} height={483}
onContentsRendered={updatingActiveRoomStore.forcedUpdate}
summary="Standalone room conversation (has-participants, 644x483)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
activeRoomStore={updatingActiveRoomStore}
roomState={ROOM_STATES.HAS_PARTICIPANTS}
isFirefox={true} />
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png" />
</div>
</Example>
</FramedExample>
<Example summary="Standalone room conversation (full - FFx user)">
<FramedExample width={644} height={483}
onContentsRendered={localFaceMuteRoomStore.forcedUpdate}
summary="Standalone room conversation (local face mute, has-participants, 644x483)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.FULL}
isFirefox={true} />
activeRoomStore={localFaceMuteRoomStore}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png" />
</div>
</Example>
</FramedExample>
<Example summary="Standalone room conversation (full - non FFx user)">
<FramedExample width={644} height={483}
onContentsRendered={remoteFaceMuteRoomStore.forcedUpdate}
summary="Standalone room conversation (remote face mute, has-participants, 644x483)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.FULL}
activeRoomStore={remoteFaceMuteRoomStore}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png" />
</div>
</FramedExample>
<FramedExample width={800} height={660}
onContentsRendered={updatingSharingRoomStore.forcedUpdate}
summary="Standalone room convo (has-participants, receivingScreenShare, 800x660)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={updatingSharingRoomStore}
roomState={ROOM_STATES.HAS_PARTICIPANTS}
isFirefox={true}
localPosterUrl="sample-img/video-screen-local.png"
remotePosterUrl="sample-img/video-screen-remote.png"
screenSharePosterUrl="sample-img/video-screen-terminal.png"
/>
</div>
</FramedExample>
<FramedExample width={644} height={483}
summary="Standalone room conversation (full - FFx user)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={fullActiveRoomStore}
isFirefox={true} />
</div>
</FramedExample>
<FramedExample width={644} height={483}
summary="Standalone room conversation (full - non FFx user)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={fullActiveRoomStore}
isFirefox={false} />
</div>
</Example>
</FramedExample>
<Example summary="Standalone room conversation (feedback)">
<FramedExample width={644} height={483}
summary="Standalone room conversation (feedback)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
activeRoomStore={endedRoomStore}
feedbackStore={feedbackStore}
roomState={ROOM_STATES.ENDED}
isFirefox={false} />
</div>
</Example>
</FramedExample>
<Example summary="Standalone room conversation (failed)">
<FramedExample width={644} height={483}
summary="Standalone room conversation (failed)">
<div className="standalone">
<StandaloneRoomView
dispatcher={dispatcher}
activeRoomStore={activeRoomStore}
roomState={ROOM_STATES.FAILED}
activeRoomStore={failedRoomStore}
isFirefox={false} />
</div>
</Example>
</FramedExample>
</Section>
<Section name="SVG icons preview" className="svg-icons">
@ -754,45 +939,6 @@
}
});
/**
* Render components that have different styles across
* CSS media rules in their own iframe to mimic the viewport
* */
function _renderComponentsInIframes() {
var parents = document.querySelectorAll(".breakpoint");
[].forEach.call(parents, appendChildInIframe);
/**
* Extracts the component from the DOM and appends in the an iframe
*
* @type {HTMLElement} parent - Parent DOM node of a component & iframe
* */
function appendChildInIframe(parent) {
var styles = document.querySelector("head").children;
var component = parent.children[0];
var iframe = document.createElement("iframe");
var width = parent.dataset.breakpointWidth;
var height = parent.dataset.breakpointHeight;
iframe.style.width = width;
iframe.style.height = height;
parent.appendChild(iframe);
iframe.src = "about:blank";
// Workaround for bug 297685
iframe.onload = function () {
var iframeHead = iframe.contentDocument.querySelector("head");
iframe.contentDocument.documentElement.querySelector("body")
.appendChild(component);
[].forEach.call(styles, function(style) {
iframeHead.appendChild(style.cloneNode(true));
});
};
}
}
window.addEventListener("DOMContentLoaded", function() {
try {
React.renderComponent(<App />, document.getElementById("main"));
@ -805,8 +951,12 @@
uncaughtError = err;
}
_renderComponentsInIframes();
// Wait until all the FramedExamples have been fully loaded.
setTimeout(function waitForQueuedFrames() {
if (window.queuedFrames.length != 0) {
setTimeout(waitForQueuedFrames, 500);
return;
}
// Put the title back, in case views changed it.
document.title = "Loop UI Components Showcase";
@ -822,6 +972,7 @@
$("#results").append("<div class='failures'><em>0</em></div>");
}
$("#results").append("<p id='complete'>Complete.</p>");
}, 1000);
});
})();

View File

@ -1113,7 +1113,10 @@ BrowserGlue.prototype = {
}
catch (ex) { /* Don't break the default prompt if telemetry is broken. */ }
Services.prefs.setBoolPref("browser.shell.isSetAsDefaultBrowser", isDefault);
if (isDefault) {
let now = Date.now().toString().slice(0, -3);
Services.prefs.setCharPref("browser.shell.mostRecentDateSetAsDefault", now);
}
if (shouldCheck && !isDefault && !willRecoverSession) {
Services.tm.mainThread.dispatch(function() {

View File

@ -34,7 +34,6 @@ browser.jar:
content/browser/pocket/panels/img/tag_close@2x.png (panels/img/tag_close@2x.png)
content/browser/pocket/panels/img/tag_closeactive@1x.png (panels/img/tag_closeactive@1x.png)
content/browser/pocket/panels/img/tag_closeactive@2x.png (panels/img/tag_closeactive@2x.png)
content/browser/pocket/panels/js/dictionary.js (panels/js/dictionary.js)
content/browser/pocket/panels/js/messages.js (panels/js/messages.js)
content/browser/pocket/panels/js/saved.js (panels/js/saved.js)
content/browser/pocket/panels/js/signup.js (panels/js/signup.js)

View File

@ -611,6 +611,19 @@ var pktUI = (function() {
}
})
});
var _initL10NMessageId = "initL10N";
pktUIMessaging.addMessageListener(_initL10NMessageId, function(panelId, data) {
var strings = {};
var bundle = Services.strings.createBundle("chrome://browser/locale/browser-pocket.properties");
var e = bundle.getSimpleEnumeration();
while(e.hasMoreElements()) {
var str = e.getNext().QueryInterface(Components.interfaces.nsIPropertyElement);
strings[str.key] = str.value;
}
pktUIMessaging.sendResponseMessageToPanel(panelId, _initL10NMessageId, { strings: strings });
});
}
// -- Browser Navigation -- //

View File

@ -1,155 +0,0 @@
Translations = {};
Translations.en =
{
addtags: "Add Tags",
alreadyhaveacct: "Already a Pocket user?",
continueff: "Continue with Firefox",
errorgeneric: "There was an error when trying to save to Pocket.",
learnmore: "Learn More",
loginnow: "Log in",
maxtaglength: "Tags are limited to 25 characters",
mustbeconnected: "You must be connected to the Internet in order to save to Pocket. Please check your connection and try again.",
onlylinkssaved: "Only links can be saved",
pagenotsaved: "Page Not Saved",
pageremoved: "Page Removed",
pagesaved: "Saved to Pocket",
processingremove: "Removing Page...",
processingtags: "Adding tags...",
removepage: "Remove Page",
save: "Save",
saving: "Saving...",
signupemail: "Sign up with email",
signuptosave: "Sign up for Pocket. Its free.",
suggestedtags: "Suggested Tags",
tagline: "Save articles and videos from Firefox to view in Pocket on any device, any time.",
taglinestory_one: "Click the Pocket Button to save any article, video or page from Firefox.",
taglinestory_two: "View in Pocket on any device, any time.",
tagssaved: "Tags Added",
signinfirefox: "Sign in with Firefox",
signupfirefox: "Sign up with Firefox",
viewlist: "View List"
};
Translations.de =
{
addtags: "Tags hinzufügen",
alreadyhaveacct: "Sind Sie bereits Pocket-Nutzer?",
continueff: "Mit Firefox fortfahren",
errorgeneric: "Beim Speichern des Links bei Pocket ist ein Fehler aufgetreten.",
learnmore: "Mehr erfahren",
loginnow: "Anmelden",
maxtaglength: "Tags dürfen höchsten 25 Zeichen lang sein.",
mustbeconnected: "Bitte überprüfen Sie, ob Sie mit dem Internet verbunden sind.",
onlylinkssaved: "Es können nur Links gespeichert werden",
pagenotsaved: "Seite nicht gespeichert",
pageremoved: "Seite entfernt",
pagesaved: "Bei Pocket gespeichert",
processingremove: "Seite wird entfernt…",
processingtags: "Tags werden hinzugefügt…",
removepage: "Seite entfernen",
save: "Speichern",
saving: "Speichern…",
signupemail: "Mit E-Mail registrieren",
signuptosave: "Registrieren Sie sich bei Pocket. Das ist kostenlos.",
suggestedtags: "Vorgeschlagene Tags",
tagline: "Speichern Sie Artikel und Videos aus Firefox bei Pocket, um sie jederzeit und auf jedem Gerät ansehen zu können.",
taglinestory_one: "Klicken Sie auf die Pocket-Schaltfläche, um beliebige Artikel, Videos und Seiten aus Firefox zu speichern.",
taglinestory_two: "Lesen Sie diese mit Pocket, jederzeit und auf jedem Gerät.",
tagssaved: "Tags hinzugefügt",
signinfirefox: "Mit Firefox anmelden",
signupfirefox: "Mit Firefox registrieren",
viewlist: "Liste anzeigen"
};
Translations.es =
{
addtags: "Añadir etiquetas",
alreadyhaveacct: "¿Ya tiene cuenta Pocket?",
continueff: "Continuar con Firefox",
errorgeneric: "Se ha producido un error al guardar el enlace en Pocket.",
learnmore: "Saber más",
loginnow: "Iniciar sesión",
maxtaglength: "Las etiquetas están limitadas a 25 caracteres.",
mustbeconnected: "Compruebe que tiene conexión a Internet.",
onlylinkssaved: "Solo se pueden guardar enlaces",
pagenotsaved: "Página no guardada",
pageremoved: "Página eliminada",
pagesaved: "Guardada en Pocket",
processingremove: "Eliminando página…",
processingtags: "Añadiendo etiquetas…",
removepage: "Eliminar página",
save: "Guardar",
saving: "Guardando…",
signupemail: "Regístrese con su correo.",
signuptosave: "Regístrese en Pocket. Es gratis.",
suggestedtags: "Etiquetas sugeridas",
tagline: "Guarde artículos y vídeos desde Firefox en Pocket para verlos en cualquier dispositivo y en cualquier momento.",
taglinestory_one: "Pulse el botón Pocket para guardar cualquier artículo, vídeo o página desde Firefox.",
taglinestory_two: "Véalo en Pocket en cualquier dispositivo y en cualquier momento.",
tagssaved: "Etiquetas añadidas",
signinfirefox: "Inicie sesión con Firefox",
signupfirefox: "Regístrese con Firefox",
viewlist: "Ver lista"
};
Translations.ja =
{
addtags: "タグを追加",
alreadyhaveacct: "アカウントをお持ちですか?",
continueff: "Firefox で続行",
errorgeneric: "Pocket にリンクを保存中に問題が発生しました。",
learnmore: "詳細",
loginnow: "ログイン",
maxtaglength: "タグは 25 文字までです。",
mustbeconnected: "インターネットに接続されていることを確認してください。",
onlylinkssaved: "リンクのみ保存できます",
pagenotsaved: "ページを保存できませんでした",
pageremoved: "ページを削除しました",
pagesaved: "Pocket に保存しました",
processingremove: "ページを削除中...",
processingtags: "タグを追加中...",
removepage: "ページを削除",
save: "保存",
saving: "保存中...",
signupemail: "メールでアカウント登録",
signuptosave: "Pocket にアカウント登録してください。無料です。",
suggestedtags: "タグ候補",
tagline: "Pocket でいつでもどこでも見られるよう、Firefox から記事や動画を保存できます。",
taglinestory_one: "Firefox から記事や動画やページを保存するには、Pocket ボタンをクリックしてください。",
taglinestory_two: "Pocket でいつでもどこでも見られます。",
tagssaved: "タグを追加しました",
signinfirefox: "Firefox でログイン",
signupfirefox: "Firefox でアカウント登録",
viewlist: "マイリストを表示"
};
Translations.ru =
{
addtags: "Добавить теги",
alreadyhaveacct: "Уже используете Pocket?",
continueff: "Продолжить через Firefox",
errorgeneric: "Не удалось сохранить в Pocket.",
learnmore: "Узнайте больше",
loginnow: "Войдите",
maxtaglength: "Длина тега не должна превышать 25 символов.",
mustbeconnected: "Убедитесь, что вы подключены к Интернет.",
onlylinkssaved: "Можно сохранять только ссылки",
pagenotsaved: "Страница не сохранена",
pageremoved: "Страница удалена",
pagesaved: "Сохранено в Pocket",
processingremove: "Удаление страницы...",
processingtags: "Добавление тегов...",
removepage: "Удалить страницу",
save: "Сохранить",
saving: "Сохранение...",
signupemail: "Регистрация по эл. почте",
signuptosave: "Зарегистрируйтесь в Pocket. Это бесплатно.",
suggestedtags: "Рекомендуемые теги",
tagline: "Сохраняйте статьи и видео из Firefox для просмотра в Pocket на любом устройстве, в любой момент.",
taglinestory_one: "Щёлкните по кнопке Pocket, чтобы сохранить любую статью, видео или страницу из Firefox.",
taglinestory_two: "Просматривайте их в Pocket на любом устройстве, в любой момент.",
tagssaved: "Теги добавлены",
signinfirefox: "Войти через Firefox",
signupfirefox: "Регистрация через Firefox",
viewlist: "Просмотреть список"
};

View File

@ -12,7 +12,6 @@ var PKT_SAVED_OVERLAY = function (options)
this.savedItemId = 0;
this.savedUrl = '';
this.premiumStatus = false;
this.panelId = 0;
this.preventCloseTimerCancel = false;
this.closeValid = true;
this.mouseInside = false;
@ -461,89 +460,7 @@ var PKT_SAVED_OVERLAY = function (options)
}
this.getTranslations = function()
{
var language = this.locale || '';
this.dictJSON = {};
var dictsuffix = 'en-US';
if (language.indexOf('en') == 0)
{
dictsuffix = 'en';
}
else if (language.indexOf('it') == 0)
{
dictsuffix = 'it';
}
else if (language.indexOf('fr-ca') == 0)
{
dictsuffix = 'fr';
}
else if (language.indexOf('fr') == 0)
{
dictsuffix = 'fr';
}
else if (language.indexOf('de') == 0)
{
dictsuffix = 'de';
}
else if (language.indexOf('es-es') == 0)
{
dictsuffix = 'es';
}
else if (language.indexOf('es-419') == 0)
{
dictsuffix = 'es_419';
}
else if (language.indexOf('es') == 0)
{
dictsuffix = 'es';
}
else if (language.indexOf('ja') == 0)
{
dictsuffix = 'ja';
}
else if (language.indexOf('nl') == 0)
{
dictsuffix = 'nl';
}
else if (language.indexOf('pt-pt') == 0)
{
dictsuffix = 'pt_PT';
}
else if (language.indexOf('pt') == 0)
{
dictsuffix = 'pt_BR';
}
else if (language.indexOf('ru') == 0)
{
dictsuffix = 'ru';
}
else if (language.indexOf('zh-tw') == 0)
{
dictsuffix = 'zh_TW';
}
else if (language.indexOf('zh') == 0)
{
dictsuffix = 'zh_CN';
}
else if (language.indexOf('ko') == 0)
{
dictsuffix = 'ko';
}
else if (language.indexOf('pl') == 0)
{
dictsuffix = 'pl';
}
this.dictJSON = Translations[dictsuffix];
if (typeof this.dictJSON !== 'object')
{
this.dictJSON = Translations['en'];
}
if (typeof this.dictJSON !== 'object')
{
this.dictJSON = {};
}
this.dictJSON = window.pocketStrings;
};
};
@ -607,17 +524,18 @@ PKT_SAVED.prototype = {
if (this.inited) {
return;
}
this.panelId = pktPanelMessaging.panelIdFromURL(window.location.href);
this.overlay = new PKT_SAVED_OVERLAY();
this.inited = true;
},
addMessageListener: function(messageId, callback) {
pktPanelMessaging.addMessageListener(this.overlay.panelId, messageId, callback);
pktPanelMessaging.addMessageListener(this.panelId, messageId, callback);
},
sendMessage: function(messageId, payload, callback) {
pktPanelMessaging.sendMessage(this.overlay.panelId, messageId, payload, callback);
pktPanelMessaging.sendMessage(this.panelId, messageId, payload, callback);
},
create: function() {
@ -643,8 +561,6 @@ PKT_SAVED.prototype = {
myself.overlay.locale = locale[1].toLowerCase();
}
myself.overlay.panelId = pktPanelMessaging.panelIdFromURL(window.location.href);
myself.overlay.create();
// tell back end we're ready
@ -686,6 +602,10 @@ $(function()
thePKT_SAVED.init();
}
// send an async message to get string data
thePKT_SAVED.sendMessage("initL10N", {}, function(resp) {
window.pocketStrings = resp.strings;
window.thePKT_SAVED.create();
});
});

View File

@ -20,7 +20,6 @@ var PKT_SIGNUP_OVERLAY = function (options)
this.inoverflowmenu = false;
this.pockethost = "getpocket.com";
this.fxasignedin = false;
this.panelId = 0;
this.dictJSON = {};
this.initCloseTabEvents = function() {
$('.btn,.pkt_ext_learnmore,.alreadyhave > a').click(function(e)
@ -58,89 +57,7 @@ var PKT_SIGNUP_OVERLAY = function (options)
};
this.getTranslations = function()
{
var language = this.locale || '';
this.dictJSON = {};
var dictsuffix = 'en-US';
if (language.indexOf('en') == 0)
{
dictsuffix = 'en';
}
else if (language.indexOf('it') == 0)
{
dictsuffix = 'it';
}
else if (language.indexOf('fr-ca') == 0)
{
dictsuffix = 'fr';
}
else if (language.indexOf('fr') == 0)
{
dictsuffix = 'fr';
}
else if (language.indexOf('de') == 0)
{
dictsuffix = 'de';
}
else if (language.indexOf('es-es') == 0)
{
dictsuffix = 'es';
}
else if (language.indexOf('es-419') == 0)
{
dictsuffix = 'es_419';
}
else if (language.indexOf('es') == 0)
{
dictsuffix = 'es';
}
else if (language.indexOf('ja') == 0)
{
dictsuffix = 'ja';
}
else if (language.indexOf('nl') == 0)
{
dictsuffix = 'nl';
}
else if (language.indexOf('pt-pt') == 0)
{
dictsuffix = 'pt_PT';
}
else if (language.indexOf('pt') == 0)
{
dictsuffix = 'pt_BR';
}
else if (language.indexOf('ru') == 0)
{
dictsuffix = 'ru';
}
else if (language.indexOf('zh-tw') == 0)
{
dictsuffix = 'zh_TW';
}
else if (language.indexOf('zh') == 0)
{
dictsuffix = 'zh_CN';
}
else if (language.indexOf('ko') == 0)
{
dictsuffix = 'ko';
}
else if (language.indexOf('pl') == 0)
{
dictsuffix = 'pl';
}
this.dictJSON = Translations[dictsuffix];
if (typeof this.dictJSON !== 'object')
{
this.dictJSON = Translations['en'];
}
if (typeof this.dictJSON !== 'object')
{
this.dictJSON = {};
}
this.dictJSON = window.pocketStrings;
};
};
@ -175,8 +92,6 @@ PKT_SIGNUP_OVERLAY.prototype = {
this.locale = locale[1].toLowerCase();
}
this.panelId = pktPanelMessaging.panelIdFromURL(window.location.href);
if (this.active)
{
return;
@ -231,17 +146,18 @@ PKT_SIGNUP.prototype = {
if (this.inited) {
return;
}
this.panelId = pktPanelMessaging.panelIdFromURL(window.location.href);
this.overlay = new PKT_SIGNUP_OVERLAY();
this.inited = true;
},
addMessageListener: function(messageId, callback) {
pktPanelMessaging.addMessageListener(this.overlay.panelId, messageId, callback);
pktPanelMessaging.addMessageListener(this.panelId, messageId, callback);
},
sendMessage: function(messageId, payload, callback) {
pktPanelMessaging.sendMessage(this.overlay.panelId, messageId, payload, callback);
pktPanelMessaging.sendMessage(this.panelId, messageId, payload, callback);
},
create: function() {
@ -260,6 +176,10 @@ $(function()
thePKT_SIGNUP.init();
}
// send an async message to get string data
thePKT_SIGNUP.sendMessage("initL10N", {}, function(resp) {
window.pocketStrings = resp.strings;
window.thePKT_SIGNUP.create();
});
});

View File

@ -10,7 +10,6 @@
<script type="text/javascript" src="js/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/vendor/handlebars.runtime.js"></script>
<script type="text/javascript" src="js/vendor/jquery.tokeninput.min.js"></script>
<script type="text/javascript" src="js/dictionary.js"></script>
<script type="text/javascript" src="js/tmpl.js"></script>
<script type="text/javascript" src="js/messages.js"></script>
<script type="text/javascript" src="js/saved.js"></script>

View File

@ -9,7 +9,6 @@
<body class="pkt_ext_containersignup" aria-live="polite">
<script type="text/javascript" src="js/vendor/jquery-2.1.1.min.js"></script>
<script type="text/javascript" src="js/vendor/handlebars.runtime.js"></script>
<script type="text/javascript" src="js/dictionary.js"></script>
<script type="text/javascript" src="js/tmpl.js"></script>
<script type="text/javascript" src="js/messages.js"></script>
<script type="text/javascript" src="js/signup.js"></script>

View File

@ -214,16 +214,6 @@ this.SessionStore = {
SessionStoreInternal.setTabState(aTab, aState);
},
// This should not be used by external code, the intention is to remove it
// once a better fix is in place for process switching in e10s.
// See bug 1075658 for context.
_restoreTabAndLoad: function ss_restoreTabAndLoad(aTab, aState, aLoadArguments) {
SessionStoreInternal.setTabState(aTab, aState, {
restoreImmediately: true,
loadArguments: aLoadArguments
});
},
duplicateTab: function ss_duplicateTab(aWindow, aTab, aDelta = 0) {
return SessionStoreInternal.duplicateTab(aWindow, aTab, aDelta);
},
@ -310,6 +300,10 @@ this.SessionStore = {
reviveCrashedTab(aTab) {
return SessionStoreInternal.reviveCrashedTab(aTab);
},
navigateAndRestore(tab, loadArguments, historyIndex) {
return SessionStoreInternal.navigateAndRestore(tab, loadArguments, historyIndex);
}
};
@ -630,12 +624,6 @@ let SessionStoreInternal = {
TabState.setSyncHandler(browser, aMessage.objects.handler);
break;
case "SessionStore:update":
// Ignore messages from <browser> elements that have crashed
// and not yet been revived.
if (this._crashedBrowsers.has(browser.permanentKey)) {
return;
}
// |browser.frameLoader| might be empty if the browser was already
// destroyed and its tab removed. In that case we still have the last
// frameLoader we know about to compare.
@ -647,12 +635,6 @@ let SessionStoreInternal = {
return;
}
// Record telemetry measurements done in the child and update the tab's
// cached state. Mark the window as dirty and trigger a delayed write.
this.recordTelemetry(aMessage.data.telemetry);
TabState.update(browser, aMessage.data);
this.saveStateDelayed(win);
if (aMessage.data.isFinal) {
// If this the final message we need to resolve all pending flush
// requests for the given browser as they might have been sent too
@ -666,6 +648,18 @@ let SessionStoreInternal = {
TabStateFlusher.resolve(browser, aMessage.data.flushID);
}
// Ignore messages from <browser> elements that have crashed
// and not yet been revived.
if (this._crashedBrowsers.has(browser.permanentKey)) {
return;
}
// Record telemetry measurements done in the child and update the tab's
// cached state. Mark the window as dirty and trigger a delayed write.
this.recordTelemetry(aMessage.data.telemetry);
TabState.update(browser, aMessage.data);
this.saveStateDelayed(win);
// Handle any updates sent by the child after the tab was closed. This
// might be the final update as sent by the "unload" handler but also
// any async update message that was sent before the child unloaded.
@ -826,6 +820,7 @@ let SessionStoreInternal = {
case "XULFrameLoaderCreated":
if (target.tagName == "browser" && target.frameLoader && target.permanentKey) {
this._lastKnownFrameLoader.set(target.permanentKey, target.frameLoader);
this.resetEpoch(target);
}
break;
default:
@ -1275,13 +1270,11 @@ let SessionStoreInternal = {
LastSession.clear();
let openWindows = {};
this._forEachBrowserWindow(function(aWindow) {
let tabs = aWindow.gBrowser.tabs;
// Remove pending or restoring tabs instead of just emptying them.
for (let i = tabs.length - 1; i >= 0 && tabs.length > 1; i--) {
if (tabs[i].linkedBrowser.__SS_restoreState) {
aWindow.gBrowser.removeTab(tabs[i], {animate: false});
}
}
Array.forEach(aWindow.gBrowser.tabs, function(aTab) {
delete aTab.linkedBrowser.__SS_data;
if (aTab.linkedBrowser.__SS_restoreState)
this._resetTabRestoringState(aTab);
}, this);
openWindows[aWindow.__SSi] = true;
});
// also clear all data about closed tabs and windows
@ -1292,9 +1285,6 @@ let SessionStoreInternal = {
delete this._windows[ix];
}
}
// Remove all pointers so that pending/restoring tabs that were closed
// above do not end up in _closedTabs[] again.
this._closedTabs.clear();
// also clear all data about closed windows
this._closedWindows = [];
// give the tabbrowsers a chance to clear their histories first
@ -1762,7 +1752,7 @@ let SessionStoreInternal = {
return JSON.stringify(tabState);
},
setTabState: function ssi_setTabState(aTab, aState, aOptions) {
setTabState(aTab, aState) {
// Remove the tab state from the cache.
// Note that we cannot simply replace the contents of the cache
// as |aState| can be an incomplete state that will be completed
@ -1787,7 +1777,7 @@ let SessionStoreInternal = {
this._resetTabRestoringState(aTab);
}
this.restoreTab(aTab, tabState, aOptions);
this.restoreTab(aTab, tabState);
},
duplicateTab: function ssi_duplicateTab(aWindow, aTab, aDelta = 0) {
@ -1819,6 +1809,13 @@ let SessionStoreInternal = {
return;
}
let window = newTab.ownerDocument && newTab.ownerDocument.defaultView;
// The tab or its window might be gone.
if (!window || !window.__SSi) {
return;
}
// Update state with flushed data. We can't use TabState.clone() here as
// the tab to duplicate may have already been closed. In that case we
// only have access to the <xul:browser>.
@ -2167,6 +2164,57 @@ let SessionStoreInternal = {
this.restoreTab(aTab, data);
},
/**
* Navigate the given |tab| by first collecting its current state and then
* either changing only the index of the currently shown shistory entry,
* or restoring the exact same state again and passing the new URL to load
* in |loadArguments|. Use this method to seamlessly switch between pages
* loaded in the parent and pages loaded in the child process.
*/
navigateAndRestore(tab, loadArguments, historyIndex) {
let window = tab.ownerDocument.defaultView;
let browser = tab.linkedBrowser;
// Set tab title to "Connecting..." and start the throbber to pretend we're
// doing something while actually waiting for data from the frame script.
window.gBrowser.setTabTitleLoading(tab);
tab.setAttribute("busy", "true");
// Flush to get the latest tab state.
TabStateFlusher.flush(browser).then(() => {
// The tab might have been closed/gone in the meantime.
if (tab.closing || !tab.linkedBrowser) {
return;
}
let window = tab.ownerDocument && tab.ownerDocument.defaultView;
// The tab or its window might be gone.
if (!window || !window.__SSi) {
return;
}
let tabState = TabState.clone(tab);
let options = {restoreImmediately: true};
if (historyIndex >= 0) {
tabState.index = historyIndex + 1;
tabState.index = Math.max(1, Math.min(tabState.index, tabState.entries.length));
} else {
tabState.userTypedValue = null;
options.loadArguments = loadArguments;
}
// Need to reset restoring tabs.
if (tab.linkedBrowser.__SS_restoreState) {
this._resetLocalTabRestoringState(tab);
}
// Restore the state into the tab.
this.restoreTab(tab, tabState, options);
});
},
/**
* See if aWindow is usable for use when restoring a previous session via
* restoreLastSession. If usable, prepare it for use.
@ -3676,6 +3724,14 @@ let SessionStoreInternal = {
return this.getCurrentEpoch(browser) == epoch;
},
/**
* Resets the epoch for a given <browser>. We need to this every time we
* receive a hint that a new docShell has been loaded into the browser as
* the frame script starts out with epoch=0.
*/
resetEpoch(browser) {
this._browserEpochs.delete(browser.permanentKey);
}
};
/**

View File

@ -35,10 +35,6 @@ this.TabState = Object.freeze({
TabStateInternal.update(browser, data);
},
flush: function (browser) {
TabStateInternal.flush(browser);
},
flushAsync: function (browser) {
TabStateInternal.flushAsync(browser);
},
@ -91,16 +87,6 @@ let TabStateInternal = {
}
},
/**
* Flushes all data currently queued in the given browser's content script.
*/
flush: function (browser) {
if (this._syncHandlers.has(browser.permanentKey)) {
let lastID = this._latestMessageID.get(browser.permanentKey);
this._syncHandlers.get(browser.permanentKey).flush(lastID);
}
},
/**
* DO NOT USE - DEBUGGING / TESTING ONLY
*
@ -118,7 +104,10 @@ let TabStateInternal = {
*/
flushWindow: function (window) {
for (let browser of window.gBrowser.browsers) {
this.flush(browser);
if (this._syncHandlers.has(browser.permanentKey)) {
let lastID = this._latestMessageID.get(browser.permanentKey);
this._syncHandlers.get(browser.permanentKey).flush(lastID);
}
}
},

View File

@ -162,7 +162,7 @@ add_task(function* save_worthy_tabs_nonremote_final() {
ok(browser.isRemoteBrowser, "browser is remote");
// Replace about:blank with a non-remote entry.
browser.loadURI("about:robots");
yield BrowserTestUtils.loadURI(browser, "about:robots");
ok(!browser.isRemoteBrowser, "browser is not remote anymore");
// Wait until the new entry replaces about:blank.

View File

@ -12,8 +12,10 @@ add_task(function test_load_start() {
let browser = tab.linkedBrowser;
yield promiseBrowserLoaded(browser);
// Load a new URI but remove the tab before it has finished loading.
browser.loadURI("about:mozilla");
// Load a new URI.
yield BrowserTestUtils.loadURI(browser, "about:mozilla");
// Remove the tab before it has finished loading.
yield promiseContentMessage(browser, "ss-test:OnHistoryReplaceEntry");
yield promiseRemoveTab(tab);

View File

@ -282,14 +282,7 @@ let promiseForEachSessionRestoreFile = Task.async(function*(cb) {
});
function promiseBrowserLoaded(aBrowser, ignoreSubFrames = true) {
return new Promise(resolve => {
aBrowser.messageManager.addMessageListener("ss-test:loadEvent", function onLoad(msg) {
if (!ignoreSubFrames || !msg.data.subframe) {
aBrowser.messageManager.removeMessageListener("ss-test:loadEvent", onLoad);
resolve();
}
});
});
return BrowserTestUtils.browserLoaded(aBrowser, !ignoreSubFrames);
}
function whenWindowLoaded(aWindow, aCallback = next) {
@ -435,8 +428,21 @@ function whenNewWindowLoaded(aOptions, aCallback) {
}
let win = openDialog(getBrowserURL(), "", "chrome,all,dialog=no" + features, url);
whenDelayedStartupFinished(win, () => aCallback(win));
return win;
let delayedStartup = promiseDelayedStartupFinished(win);
let browserLoaded = new Promise(resolve => {
if (url == "about:blank") {
resolve();
return;
}
win.addEventListener("load", function onLoad() {
win.removeEventListener("load", onLoad);
resolve(promiseBrowserLoaded(win.gBrowser.selectedBrowser));
});
});
Promise.all([delayedStartup, browserLoaded]).then(() => aCallback(win));
}
function promiseNewWindowLoaded(aOptions) {
return new Promise(resolve => whenNewWindowLoaded(aOptions, resolve));

View File

@ -70,8 +70,7 @@ const TARGET_SEARCHENGINE_PREFIX = "searchEngine-";
XPCOMUtils.defineLazyGetter(this, "log", () => {
let ConsoleAPI = Cu.import("resource://gre/modules/devtools/Console.jsm", {}).ConsoleAPI;
let consoleOptions = {
// toLowerCase is because the loglevel values use title case to be compatible with Log.jsm.
maxLogLevel: Services.prefs.getCharPref(PREF_LOG_LEVEL).toLowerCase(),
maxLogLevelPref: PREF_LOG_LEVEL,
prefix: "UITour",
};
return new ConsoleAPI(consoleOptions);

View File

@ -39,6 +39,10 @@ XPCOMUtils.defineLazyModuleGetter(this, "NetUtil",
XPCOMUtils.defineLazyModuleGetter(this, "DevToolsUtils",
"resource://gre/modules/devtools/DevToolsUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function() {
return require("devtools/toolkit/webconsole/network-helper");
});
// The panel's window global is an EventEmitter firing the following events:
const EVENTS = {
// When the UI is reset from tab navigation.
@ -193,27 +197,12 @@ EventEmitter.decorate(this);
let $ = (selector, target = document) => target.querySelector(selector);
let $all = (selector, target = document) => target.querySelectorAll(selector);
/**
* Helper for getting an nsIURL instance out of a string.
*/
function nsIURL(url, store = nsIURL.store) {
if (store.has(url)) {
return store.get(url);
}
let uri = Services.io.newURI(url, null, null).QueryInterface(Ci.nsIURL);
store.set(url, uri);
return uri;
}
// The cache used in the `nsIURL` function.
nsIURL.store = new Map();
/**
* Gets the fileName part of a string which happens to be an URL.
*/
function getFileName(url) {
try {
let { fileName } = nsIURL(url);
let { fileName } = NetworkHelper.nsIURL(url);
return fileName || "/";
} catch (e) {
// This doesn't look like a url, or nsIURL can't handle it.

View File

@ -88,7 +88,10 @@ Tools.inspector = {
url: "chrome://browser/content/devtools/inspector/inspector.xul",
label: l10n("inspector.label", inspectorStrings),
panelLabel: l10n("inspector.panelLabel", inspectorStrings),
tooltip: l10n("inspector.tooltip", inspectorStrings),
get tooltip() {
return l10n("inspector.tooltip2", inspectorStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
commands: [
"devtools/resize-commands",
@ -122,7 +125,10 @@ Tools.webConsole = {
label: l10n("ToolboxTabWebconsole.label", webConsoleStrings),
menuLabel: l10n("MenuWebconsole.label", webConsoleStrings),
panelLabel: l10n("ToolboxWebConsole.panelLabel", webConsoleStrings),
tooltip: l10n("ToolboxWebconsole.tooltip", webConsoleStrings),
get tooltip() {
return l10n("ToolboxWebconsole.tooltip2", webConsoleStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
commands: "devtools/webconsole/console-commands",
@ -155,7 +161,10 @@ Tools.jsdebugger = {
url: "chrome://browser/content/devtools/debugger.xul",
label: l10n("ToolboxDebugger.label", debuggerStrings),
panelLabel: l10n("ToolboxDebugger.panelLabel", debuggerStrings),
tooltip: l10n("ToolboxDebugger.tooltip", debuggerStrings),
get tooltip() {
return l10n("ToolboxDebugger.tooltip2", debuggerStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
commands: "devtools/debugger/debugger-commands",
@ -179,7 +188,10 @@ Tools.styleEditor = {
url: "chrome://browser/content/devtools/styleeditor.xul",
label: l10n("ToolboxStyleEditor.label", styleEditorStrings),
panelLabel: l10n("ToolboxStyleEditor.panelLabel", styleEditorStrings),
tooltip: l10n("ToolboxStyleEditor.tooltip2", styleEditorStrings),
get tooltip() {
return l10n("ToolboxStyleEditor.tooltip3", styleEditorStrings,
"Shift+" + functionkey(this.key));
},
inMenu: true,
commands: "devtools/styleeditor/styleeditor-commands",
@ -244,7 +256,10 @@ Tools.performance = {
visibilityswitch: "devtools.performance.enabled",
label: l10n("profiler.label2", profilerStrings),
panelLabel: l10n("profiler.panelLabel2", profilerStrings),
tooltip: l10n("profiler.tooltip2", profilerStrings),
get tooltip() {
return l10n("profiler.tooltip3", profilerStrings,
"Shift+" + functionkey(this.key));
},
accesskey: l10n("profiler.accesskey", profilerStrings),
key: l10n("profiler.commandkey2", profilerStrings),
modifiers: "shift",
@ -271,7 +286,10 @@ Tools.netMonitor = {
url: "chrome://browser/content/devtools/netmonitor.xul",
label: l10n("netmonitor.label", netMonitorStrings),
panelLabel: l10n("netmonitor.panelLabel", netMonitorStrings),
tooltip: l10n("netmonitor.tooltip", netMonitorStrings),
get tooltip() {
return l10n("netmonitor.tooltip2", netMonitorStrings,
( osString == "Darwin" ? "Cmd+Alt" : "Ctrl+Shift+" ) + this.key);
},
inMenu: true,
isTargetSupported: function(target) {
@ -296,7 +314,10 @@ Tools.storage = {
label: l10n("storage.label", storageStrings),
menuLabel: l10n("storage.menuLabel", storageStrings),
panelLabel: l10n("storage.panelLabel", storageStrings),
tooltip: l10n("storage.tooltip2", storageStrings),
get tooltip() {
return l10n("storage.tooltip3", storageStrings,
"Shift+" + functionkey(this.key));
},
inMenu: true,
isTargetSupported: function(target) {
@ -399,12 +420,18 @@ exports.defaultThemes = [
* The key to lookup.
* @returns A localized version of the given key.
*/
function l10n(name, bundle)
function l10n(name, bundle, arg)
{
try {
return bundle.GetStringFromName(name);
return arg ? bundle.formatStringFromName(name, [arg], 1)
: bundle.GetStringFromName(name);
} catch (ex) {
Services.console.logStringMessage("Error reading '" + name + "'");
throw new Error("l10n error with " + name);
}
}
function functionkey(shortkey)
{
return shortkey.split("_")[1];
}

View File

@ -55,7 +55,7 @@ loader.lazyGetter(this, "toolboxStrings", () => {
loader.lazyGetter(this, "Selection", () => require("devtools/framework/selection").Selection);
loader.lazyGetter(this, "InspectorFront", () => require("devtools/server/actors/inspector").InspectorFront);
loader.lazyRequireGetter(this, "DevToolsUtils", "devtools/toolkit/DevToolsUtils");
loader.lazyRequireGetter(this, "getPerformanceActorsConnection", "devtools/performance/front", true);
loader.lazyRequireGetter(this, "getPerformanceFront", "devtools/performance/front", true);
XPCOMUtils.defineLazyGetter(this, "screenManager", () => {
return Cc["@mozilla.org/gfx/screenmanager;1"].getService(Ci.nsIScreenManager);
@ -287,6 +287,14 @@ Toolbox.prototype = {
return this._highlighter;
},
/**
* Get the toolbox's performance front. Note that it may not always have been
* initialized first. Use `initPerformance()` if needed.
*/
get performance() {
return this._performance;
},
/**
* Get the toolbox's inspector front. Note that it may not always have been
* initialized first. Use `initInspector()` if needed.
@ -398,9 +406,8 @@ Toolbox.prototype = {
]);
// Lazily connect to the profiler here and don't wait for it to complete,
// used to intercept console.profile calls before the performance tools
// are open.
let profilerReady = this._connectProfiler();
// used to intercept console.profile calls before the performance tools are open.
let profilerReady = this.initPerformance();
// However, while testing, we must wait for the performance connection to
// finish, as most tests shut down without waiting for a toolbox
@ -1889,7 +1896,7 @@ Toolbox.prototype = {
}));
// Destroy the profiler connection
outstanding.push(this._disconnectProfiler());
outstanding.push(this.destroyPerformance());
// We need to grab a reference to win before this._host is destroyed.
let win = this.frame.ownerGlobal;
@ -1982,27 +1989,28 @@ Toolbox.prototype = {
"cmd_copy", "cmd_paste", "cmd_selectAll"].forEach(window.goUpdateCommand);
},
getPerformanceActorsConnection: function() {
if (!this._performanceConnection) {
this._performanceConnection = getPerformanceActorsConnection(this.target);
}
return this._performanceConnection;
},
/**
* Connects to the SPS profiler when the developer tools are open. This is
* necessary because of the WebConsole's `profile` and `profileEnd` methods.
*/
_connectProfiler: Task.async(function*() {
initPerformance: Task.async(function*() {
// If target does not have profiler actor (addons), do not
// even register the shared performance connection.
if (!this.target.hasActor("profiler")) {
return;
}
yield this.getPerformanceActorsConnection().open();
if (this.performance) {
yield this.performance.open();
return this.performance;
}
this._performance = getPerformanceFront(this.target);
yield this.performance.open();
// Emit an event when connected, but don't wait on startup for this.
this.emit("profiler-connected");
return this.performance;
}),
/**
@ -2010,12 +2018,12 @@ Toolbox.prototype = {
* has not finished initializing, as opening a toolbox does not wait,
* the performance connection destroy method will wait for it on its own.
*/
_disconnectProfiler: Task.async(function*() {
if (!this._performanceConnection) {
destroyPerformance: Task.async(function*() {
if (!this.performance) {
return;
}
yield this._performanceConnection.destroy();
this._performanceConnection = null;
yield this.performance.destroy();
this._performance = null;
}),
/**

View File

@ -28,6 +28,8 @@ loader.lazyGetter(this, "clipboardHelper", () => {
return Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper);
});
loader.lazyImporter(this, "CommandUtils", "resource:///modules/devtools/DeveloperToolbar.jsm");
const LAYOUT_CHANGE_TIMER = 250;
/**
@ -652,6 +654,9 @@ InspectorPanel.prototype = {
!this.selection.isPseudoElementNode();
let isEditableElement = isSelectionElement &&
!this.selection.isAnonymousNode();
let isScreenshotable = isSelectionElement &&
this.canGetUniqueSelector &&
this.selection.nodeFront.isTreeDisplayed;
// Set the pseudo classes
for (let name of ["hover", "active", "focus"]) {
@ -675,8 +680,9 @@ InspectorPanel.prototype = {
}
// Disable / enable "Copy Unique Selector", "Copy inner HTML",
// "Copy outer HTML" & "Scroll Into View" as appropriate
// "Copy outer HTML", "Scroll Into View" & "Screenshot Node" as appropriate
let unique = this.panelDoc.getElementById("node-menu-copyuniqueselector");
let screenshot = this.panelDoc.getElementById("node-menu-screenshotnode");
let copyInnerHTML = this.panelDoc.getElementById("node-menu-copyinner");
let copyOuterHTML = this.panelDoc.getElementById("node-menu-copyouter");
let scrollIntoView = this.panelDoc.getElementById("node-menu-scrollnodeintoview");
@ -700,6 +706,12 @@ InspectorPanel.prototype = {
unique.hidden = true;
}
if (isScreenshotable) {
screenshot.removeAttribute("disabled");
} else {
screenshot.setAttribute("disabled", "true");
}
// Enable/Disable the link open/copy items.
this._setupNodeLinkMenu();
@ -1068,6 +1080,17 @@ InspectorPanel.prototype = {
}).then(null, console.error);
},
/**
* Initiate gcli screenshot command on selected node
*/
screenshotNode: function() {
CommandUtils.createRequisition(this._target, {
environment: CommandUtils.createEnvironment(this, '_target')
}).then(requisition => {
requisition.updateExec("screenshot --selector " + this.selectionCssSelector);
});
},
/**
* Scroll the node into view.
*/

View File

@ -91,6 +91,9 @@
label="&inspectorScrollNodeIntoView.label;"
accesskey="&inspectorScrollNodeIntoView.accesskey;"
oncommand="inspector.scrollNodeIntoView()"/>
<menuitem id="node-menu-screenshotnode"
label="&inspectorScreenshotNode.label;"
oncommand="inspector.screenshotNode()" />
<menuitem id="node-menu-delete"
label="&inspectorHTMLDelete.label;"
accesskey="&inspectorHTMLDelete.accesskey;"

View File

@ -27,7 +27,8 @@ const ALL_MENU_ITEMS = [
"node-menu-pseudo-hover",
"node-menu-pseudo-active",
"node-menu-pseudo-focus",
"node-menu-scrollnodeintoview"
"node-menu-scrollnodeintoview",
"node-menu-screenshotnode"
].concat(PASTE_MENU_ITEMS);
const ITEMS_WITHOUT_SHOWDOMPROPS =
@ -93,7 +94,16 @@ const TEST_CASES = [
"node-menu-copyimagedatauri",
"node-menu-pastebefore",
"node-menu-pasteafter",
]
"node-menu-screenshotnode",
],
},
{
desc: "<head> with no html on clipboard",
selector: "head",
disabled: PASTE_MENU_ITEMS.concat([
"node-menu-copyimagedatauri",
"node-menu-screenshotnode",
]),
},
{
desc: "<element> with text on clipboard",
@ -125,6 +135,22 @@ const TEST_CASES = [
selector: "#paste-area",
disabled: PASTE_MENU_ITEMS.concat(["node-menu-copyimagedatauri"]),
},
{
desc: "<element> that isn't visible on the page, empty clipboard",
selector: "#hiddenElement",
disabled: PASTE_MENU_ITEMS.concat([
"node-menu-copyimagedatauri",
"node-menu-screenshotnode",
]),
},
{
desc: "<element> nested in another hidden element, empty clipboard",
selector: "#nestedHiddenElement",
disabled: PASTE_MENU_ITEMS.concat([
"node-menu-copyimagedatauri",
"node-menu-screenshotnode",
]),
}
];
let clipboard = require("sdk/clipboard");

View File

@ -17,6 +17,9 @@
<p id="sensitivity">Paragraph for sensitivity</p>
<p id="delete">This has to be deleted</p>
<img id="copyimage" src="data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAAAAAA6fptVAAAACklEQVQYV2P4DwABAQEAWk1v8QAAAABJRU5ErkJggg==" />
<div id="hiddenElement" style="display: none;">
<p id="nestedHiddenElement">Visible element nested inside a non-visible element</p>
</div>
</div>
</body>
</html>

View File

@ -0,0 +1,476 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Ci, Cc } = require("chrome");
const { defer, all, resolve } = require("sdk/core/promise");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { devtools } = Cu.import("resource://gre/modules/devtools/Loader.jsm", {});
const { XPCOMUtils } = Cu.import("resource://gre/modules/XPCOMUtils.jsm", {});
XPCOMUtils.defineLazyGetter(this, "appInfo", function() {
return Cc["@mozilla.org/xre/app-info;1"].getService(Ci.nsIXULAppInfo);
});
XPCOMUtils.defineLazyModuleGetter(this, "ViewHelpers",
"resource:///modules/devtools/ViewHelpers.jsm");
XPCOMUtils.defineLazyGetter(this, "L10N", function() {
return new ViewHelpers.L10N("chrome://browser/locale/devtools/har.properties");
});
XPCOMUtils.defineLazyGetter(this, "NetworkHelper", function() {
return devtools.require("devtools/toolkit/webconsole/network-helper");
});
const HAR_VERSION = "1.1";
/**
* This object is responsible for building HAR file. See HAR spec:
* https://dvcs.w3.org/hg/webperf/raw-file/tip/specs/HAR/Overview.html
* http://www.softwareishard.com/blog/har-12-spec/
*
* @param {Object} options configuration object
*
* The following options are supported:
*
* - items {Array}: List of Network requests to be exported. It is possible
* to use directly: NetMonitorView.RequestsMenu.items
*
* - id {String}: ID of the exported page.
*
* - title {String}: Title of the exported page.
*
* - includeResponseBodies {Boolean}: Set to true to include HTTP response
* bodies in the result data structure.
*/
var HarBuilder = function(options) {
this._options = options;
this._pageMap = [];
}
HarBuilder.prototype = {
// Public API
/**
* This is the main method used to build the entire result HAR data.
* The process is asynchronous since it can involve additional RDP
* communication (e.g. resolving long strings).
*
* @returns {Promise} A promise that resolves to the HAR object when
* the entire build process is done.
*/
build: function() {
this.promises = [];
// Build basic structure for data.
let log = this.buildLog();
// Build entries.
let items = this._options.items;
for (let i=0; i<items.length; i++) {
let file = items[i].attachment;
log.entries.push(this.buildEntry(log, file));
}
// Some data needs to be fetched from the backend during the
// build process, so wait till all is done.
let { resolve, promise } = defer();
all(this.promises).then(results => resolve({ log: log }));
return promise;
},
// Helpers
buildLog: function() {
return {
version: HAR_VERSION,
creator: {
name: appInfo.name,
version: appInfo.version
},
browser: {
name: appInfo.name,
version: appInfo.version
},
pages: [],
entries: [],
}
},
buildPage: function(file) {
let page = {};
// Page start time is set when the first request is processed
// (see buildEntry)
page.startedDateTime = 0;
page.id = "page_" + this._options.id;
page.title = this._options.title;
return page;
},
getPage: function(log, file) {
let id = this._options.id;
let page = this._pageMap[id];
if (page) {
return page;
}
this._pageMap[id] = page = this.buildPage(file);
log.pages.push(page);
return page;
},
buildEntry: function(log, file) {
let page = this.getPage(log, file);
let entry = {};
entry.pageref = page.id;
entry.startedDateTime = dateToJSON(new Date(file.startedMillis));
entry.time = file.endedMillis - file.startedMillis;
entry.request = this.buildRequest(file);
entry.response = this.buildResponse(file);
entry.cache = this.buildCache(file);
entry.timings = file.eventTimings ? file.eventTimings.timings : {};
if (file.remoteAddress) {
entry.serverIPAddress = file.remoteAddress;
}
if (file.remotePort) {
entry.connection = file.remotePort + "";
}
// Compute page load start time according to the first request start time.
if (!page.startedDateTime) {
page.startedDateTime = entry.startedDateTime;
page.pageTimings = this.buildPageTimings(page, file);
}
return entry;
},
buildPageTimings: function(page, file) {
// Event timing info isn't available
let timings = {
onContentLoad: -1,
onLoad: -1
};
return timings;
},
buildRequest: function(file) {
let request = {
bodySize: 0
};
request.method = file.method;
request.url = file.url;
request.httpVersion = file.httpVersion;
request.headers = this.buildHeaders(file.requestHeaders);
request.cookies = this.buildCookies(file.requestCookies);
request.queryString = NetworkHelper.parseQueryString(
NetworkHelper.nsIURL(file.url).query) || [];
request.postData = this.buildPostData(file);
request.headersSize = file.requestHeaders.headersSize;
// Set request body size, but make sure the body is fetched
// from the backend.
if (file.requestPostData) {
this.fetchData(file.requestPostData.postData.text).then(value => {
request.bodySize = value.length;
});
}
return request;
},
/**
* Fetch all header values from the backend (if necessary) and
* build the result HAR structure.
*
* @param {Object} input Request or response header object.
*/
buildHeaders: function(input) {
if (!input) {
return [];
}
return this.buildNameValuePairs(input.headers);
},
buildCookies: function(input) {
if (!input) {
return [];
}
return this.buildNameValuePairs(input.cookies);
},
buildNameValuePairs: function(entries) {
let result = [];
// HAR requires headers array to be presented, so always
// return at least an empty array.
if (!entries) {
return result;
}
// Make sure header values are fully fetched from the server.
entries.forEach(entry => {
this.fetchData(entry.value).then(value => {
result.push({
name: entry.name,
value: value
});
});
})
return result;
},
buildPostData: function(file) {
let postData = {
mimeType: findValue(file.requestHeaders.headers, "content-type"),
params: [],
text: ""
};
if (!file.requestPostData) {
return postData;
}
if (file.requestPostData.postDataDiscarded) {
postData.comment = L10N.getStr("har.requestBodyNotIncluded");
return postData;
}
// Load request body from the backend.
this.fetchData(file.requestPostData.postData.text).then(value => {
postData.text = value;
// If we are dealing with URL encoded body, parse parameters.
if (isURLEncodedFile(file, value)) {
postData.mimeType = "application/x-www-form-urlencoded";
// Extract form parameters and produce nice HAR array.
this._options.view._getFormDataSections(file.requestHeaders,
file.requestHeadersFromUploadStream,
file.requestPostData).then(formDataSections => {
formDataSections.forEach(section => {
let paramsArray = NetworkHelper.parseQueryString(section);
if (paramsArray) {
postData.params = [...postData.params, ...paramsArray];
}
});
});
}
});
return postData;
},
buildResponse: function(file) {
let response = {
status: 0
};
// Arbitrary value if it's aborted to make sure status has a number
if (file.status) {
response.status = parseInt(file.status);
}
response.statusText = file.statusText || "";
response.httpVersion = file.httpVersion;
response.headers = this.buildHeaders(file.responseHeaders);
response.cookies = this.buildCookies(file.responseCookies);
response.content = this.buildContent(file);
response.redirectURL = findValue(file.responseHeaders.headers, "Location");
response.headersSize = file.responseHeaders.headersSize;
response.bodySize = file.transferredSize || -1;
return response;
},
buildContent: function(file) {
let content = {
mimeType: file.mimeType,
size: -1
};
if (file.responseContent && file.responseContent.content) {
content.size = file.responseContent.content.size;
}
if (!this._options.includeResponseBodies ||
file.responseContent.contentDiscarded) {
content.comment = L10N.getStr("har.responseBodyNotIncluded");
return content;
}
if (file.responseContent) {
let text = file.responseContent.content.text;
let promise = this.fetchData(text).then(value => {
content.text = value;
});
}
return content;
},
buildCache: function(file) {
let cache = {};
if (!file.fromCache) {
return cache;
}
// There is no such info yet in the Net panel.
// cache.beforeRequest = {};
if (file.cacheEntry) {
cache.afterRequest = this.buildCacheEntry(file.cacheEntry);
} else {
cache.afterRequest = null;
}
return cache;
},
buildCacheEntry: function(cacheEntry) {
let cache = {};
cache.expires = findValue(cacheEntry, "Expires");
cache.lastAccess = findValue(cacheEntry, "Last Fetched");
cache.eTag = "";
cache.hitCount = findValue(cacheEntry, "Fetch Count");
return cache;
},
getBlockingEndTime: function(file) {
if (file.resolveStarted && file.connectStarted) {
return file.resolvingTime;
}
if (file.connectStarted) {
return file.connectingTime;
}
if (file.sendStarted) {
return file.sendingTime;
}
return (file.sendingTime > file.startTime) ?
file.sendingTime : file.waitingForTime;
},
// RDP Helpers
fetchData: function(string) {
let promise = this._options.getString(string).then(value => {
return value;
});
// Building HAR is asynchronous and not done till all
// collected promises are resolved.
this.promises.push(promise);
return promise;
}
}
// Helpers
/**
* Returns true if specified request body is URL encoded.
*/
function isURLEncodedFile(file, text) {
let contentType = "content-type: application/x-www-form-urlencoded"
if (text && text.toLowerCase().indexOf(contentType) != -1) {
return true;
}
// The header value doesn't have to be always exactly
// "application/x-www-form-urlencoded",
// there can be even charset specified. So, use indexOf rather than just
// "==".
let value = findValue(file.requestHeaders.headers, "content-type");
if (value && value.indexOf("application/x-www-form-urlencoded") == 0) {
return true;
}
return false;
}
/**
* Find specified value within an array of name-value pairs
* (used for headers, cookies and cache entries)
*/
function findValue(arr, name) {
name = name.toLowerCase();
let result = arr.find(entry => entry.name.toLowerCase() == name);
return result ? result.value : "";
}
/**
* Generate HAR representation of a date.
* (YYYY-MM-DDThh:mm:ss.sTZD, e.g. 2009-07-24T19:20:30.45+01:00)
* See also HAR Schema: http://janodvarko.cz/har/viewer/
*
* Note: it would be great if we could utilize Date.toJSON(), but
* it doesn't return proper time zone offset.
*
* An example:
* This helper returns: 2015-05-29T16:10:30.424+02:00
* Date.toJSON() returns: 2015-05-29T14:10:30.424Z
*
* @param date {Date} The date object we want to convert.
*/
function dateToJSON(date) {
function f(n, c) {
if (!c) {
c = 2;
}
let s = new String(n);
while (s.length < c) {
s = "0" + s;
}
return s;
}
let result = date.getFullYear() + '-' +
f(date.getMonth() + 1) + '-' +
f(date.getDate()) + 'T' +
f(date.getHours()) + ':' +
f(date.getMinutes()) + ':' +
f(date.getSeconds()) + '.' +
f(date.getMilliseconds(), 3);
let offset = date.getTimezoneOffset();
let positive = offset > 0;
// Convert to positive number before using Math.floor (see issue 5512)
offset = Math.abs(offset);
let offsetHours = Math.floor(offset / 60);
let offsetMinutes = Math.floor(offset % 60);
let prettyOffset = (positive > 0 ? "-" : "+") + f(offsetHours) +
":" + f(offsetMinutes);
return result + prettyOffset;
}
// Exports from this module
exports.HarBuilder = HarBuilder;

View File

@ -0,0 +1,175 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Cc, Ci } = require("chrome");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
const { defer, resolve } = require("sdk/core/promise");
const { HarUtils } = require("./har-utils.js");
const { HarBuilder } = require("./har-builder.js");
XPCOMUtils.defineLazyGetter(this, "clipboardHelper", function() {
return Cc["@mozilla.org/widget/clipboardhelper;1"].
getService(Ci.nsIClipboardHelper);
});
var uid = 1;
/**
* This object represents the main public API designed to access
* Network export logic. Clients, such as the Network panel itself,
* should use this API to export collected HTTP data from the panel.
*/
const HarExporter = {
// Public API
/**
* Save collected HTTP data from the Network panel into HAR file.
*
* @param Object options
* Configuration object
*
* The following options are supported:
*
* - includeResponseBodies {Boolean}: If set to true, HTTP response bodies
* are also included in the HAR file (can produce significantly bigger
* amount of data).
*
* - items {Array}: List of Network requests to be exported. It is possible
* to use directly: NetMonitorView.RequestsMenu.items
*
* - jsonp {Boolean}: If set to true the export format is HARP (support
* for JSONP syntax).
*
* - jsonpCallback {String}: Default name of JSONP callback (used for
* HARP format).
*
* - compress {Boolean}: If set to true the final HAR file is zipped.
* This represents great disk-space optimization.
*
* - defaultFileName {String}: Default name of the target HAR file.
* The default file name supports formatters, see:
* https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
*
* - defaultLogDir {String}: Default log directory for automated logs.
*
* - id {String}: ID of the page (used in the HAR file).
*
* - title {String}: Title of the page (used in the HAR file).
*
* - forceExport {Boolean}: The result HAR file is created even if
* there are no HTTP entries.
*/
save: function(options) {
// Set default options related to save operation.
options.defaultFileName = Services.prefs.getCharPref(
"devtools.netmonitor.har.defaultFileName");
options.compress = Services.prefs.getBoolPref(
"devtools.netmonitor.har.compress");
// Get target file for exported data. Bail out, if the user
// presses cancel.
let file = HarUtils.getTargetFile(options.defaultFileName,
options.jsonp, options.compress);
if (!file) {
return resolve();
}
return this.fetchHarData(options).then(jsonString => {
if (!HarUtils.saveToFile(file, jsonString, options.compress)) {
let msg = "Failed to save HAR file at: " + options.defaultFileName;
Cu.reportError(msg);
}
return jsonString;
});
},
/**
* Copy HAR string into the clipboard.
*
* @param Object options
* Configuration object, see save() for detailed description.
*/
copy: function(options) {
return this.fetchHarData(options).then(jsonString => {
clipboardHelper.copyString(jsonString);
return jsonString;
});
},
// Helpers
fetchHarData: function(options) {
// Generate page ID
options.id = options.id || uid++;
// Set default generic HAR export options.
options.jsonp = options.jsonp ||
Services.prefs.getBoolPref("devtools.netmonitor.har.jsonp");
options.includeResponseBodies = options.includeResponseBodies ||
Services.prefs.getBoolPref("devtools.netmonitor.har.includeResponseBodies");
options.jsonpCallback = options.jsonpCallback ||
Services.prefs.getCharPref( "devtools.netmonitor.har.jsonpCallback");
options.forceExport = options.forceExport ||
Services.prefs.getBoolPref("devtools.netmonitor.har.forceExport");
// Build HAR object.
return this.buildHarData(options).then(har => {
// Do not export an empty HAR file, unless the user
// explicitly says so (using the forceExport option).
if (!har.log.entries.length && !options.forceExport) {
return resolve();
}
let jsonString = this.stringify(har);
if (!jsonString) {
return resolve();
}
// If JSONP is wanted, wrap the string in a function call
if (options.jsonp) {
// This callback name is also used in HAR Viewer by default.
// http://www.softwareishard.com/har/viewer/
let callbackName = options.jsonpCallback || "onInputData";
jsonString = callbackName + "(" + jsonString + ");";
}
return jsonString;
}).then(null, function onError(err) {
Cu.reportError(err);
});
},
/**
* Build HAR data object. This object contains all HTTP data
* collected by the Network panel. The process is asynchronous
* since it can involve additional RDP communication (e.g. resolving
* long strings).
*/
buildHarData: function(options) {
// Build HAR object from collected data.
let builder = new HarBuilder(options);
return builder.build();
},
/**
* Build JSON string from the HAR data object.
*/
stringify: function(har) {
if (!har) {
return null;
}
try {
return JSON.stringify(har, null, " ");
}
catch (err) {
Cu.reportError(err);
}
},
};
// Exports from this module
exports.HarExporter = HarExporter;

View File

@ -0,0 +1,185 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const { Cu, Ci, Cc, CC } = require("chrome");
const { Services } = Cu.import("resource://gre/modules/Services.jsm", {});
XPCOMUtils.defineLazyGetter(this, "dirService", function() {
return Cc["@mozilla.org/file/directory_service;1"].getService(Ci.nsIProperties);
});
XPCOMUtils.defineLazyGetter(this, "ZipWriter", function() {
return CC("@mozilla.org/zipwriter;1", "nsIZipWriter");
});
XPCOMUtils.defineLazyGetter(this, "LocalFile", function() {
return new CC("@mozilla.org/file/local;1", "nsILocalFile", "initWithPath");
});
XPCOMUtils.defineLazyGetter(this, "getMostRecentBrowserWindow", function() {
return require("sdk/window/utils").getMostRecentBrowserWindow;
});
const nsIFilePicker = Ci.nsIFilePicker;
const OPEN_FLAGS = {
RDONLY: parseInt("0x01"),
WRONLY: parseInt("0x02"),
CREATE_FILE: parseInt("0x08"),
APPEND: parseInt("0x10"),
TRUNCATE: parseInt("0x20"),
EXCL: parseInt("0x80")
};
/**
* Helper API for HAR export features.
*/
var HarUtils = {
/**
* Open File Save As dialog and let the user pick the proper file
* location for generated HAR log.
*/
getTargetFile: function(fileName, jsonp, compress) {
let browser = getMostRecentBrowserWindow();
let fp = Cc["@mozilla.org/filepicker;1"].createInstance(nsIFilePicker);
fp.init(browser, null, nsIFilePicker.modeSave);
fp.appendFilter("HTTP Archive Files", "*.har; *.harp; *.json; *.jsonp; *.zip");
fp.appendFilters(nsIFilePicker.filterAll | nsIFilePicker.filterText);
fp.filterIndex = 1;
fp.defaultString = this.getHarFileName(fileName, jsonp, compress);
let rv = fp.show();
if (rv == nsIFilePicker.returnOK || rv == nsIFilePicker.returnReplace) {
return fp.file;
}
return null;
},
getHarFileName: function(defaultFileName, jsonp, compress) {
let extension = jsonp ? ".harp" : ".har";
// Read more about toLocaleFormat & format string.
// https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Date/toLocaleFormat
var now = new Date();
var name = now.toLocaleFormat(defaultFileName);
name = name.replace(/\:/gm, "-", "");
name = name.replace(/\//gm, "_", "");
let fileName = name + extension;
// Default file extension is zip if compressing is on.
if (compress) {
fileName += ".zip";
}
return fileName;
},
/**
* Save HAR string into a given file. The file might be compressed
* if specified in the options.
*
* @param {File} file Target file where the HAR string (JSON)
* should be stored.
* @param {String} jsonString HAR data (JSON or JSONP)
* @param {Boolean} compress The result file is zipped if set to true.
*/
saveToFile: function(file, jsonString, compress) {
let openFlags = OPEN_FLAGS.WRONLY | OPEN_FLAGS.CREATE_FILE |
OPEN_FLAGS.TRUNCATE;
try {
let foStream = Cc["@mozilla.org/network/file-output-stream;1"]
.createInstance(Ci.nsIFileOutputStream);
let permFlags = parseInt("0666", 8);
foStream.init(file, openFlags, permFlags, 0);
let convertor = Cc["@mozilla.org/intl/converter-output-stream;1"]
.createInstance(Ci.nsIConverterOutputStream);
convertor.init(foStream, "UTF-8", 0, 0);
// The entire jsonString can be huge so, write the data in chunks.
let chunkLength = 1024 * 1024;
for (let i=0; i<=jsonString.length; i++) {
let data = jsonString.substr(i, chunkLength+1);
if (data) {
convertor.writeString(data);
}
i = i + chunkLength;
}
// this closes foStream
convertor.close();
} catch (err) {
Cu.reportError(err);
return false;
}
// If no compressing then bail out.
if (!compress) {
return true;
}
// Remember name of the original file, it'll be replaced by a zip file.
let originalFilePath = file.path;
let originalFileName = file.leafName;
try {
// Rename using unique name (the file is going to be removed).
file.moveTo(null, "temp" + (new Date()).getTime() + "temphar");
// Create compressed file with the original file path name.
let zipFile = Cc["@mozilla.org/file/local;1"].
createInstance(Ci.nsILocalFile);
zipFile.initWithPath(originalFilePath);
// The file within the zipped file doesn't use .zip extension.
let fileName = originalFileName;
if (fileName.indexOf(".zip") == fileName.length - 4) {
fileName = fileName.substr(0, fileName.indexOf(".zip"));
}
let zip = new ZipWriter();
zip.open(zipFile, openFlags);
zip.addEntryFile(fileName, Ci.nsIZipWriter.COMPRESSION_DEFAULT,
file, false);
zip.close();
// Remove the original file (now zipped).
file.remove(true);
return true;
} catch (err) {
Cu.reportError(err);
// Something went wrong (disk space?) rename the original file back.
file.moveTo(null, originalFileName);
}
return false;
},
getLocalDirectory: function(path) {
let dir;
if (!path) {
dir = dirService.get("ProfD", Ci.nsILocalFile);
dir.append("har");
dir.append("logs");
} else {
dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsILocalFile);
dir.initWithPath(path);
}
return dir;
},
}
// Exports from this module
exports.HarUtils = HarUtils;

Some files were not shown because too many files have changed in this diff Show More