mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-11-30 08:12:05 +00:00
Merge m-c to b2ginbound, a=merge
This commit is contained in:
commit
68f351b859
@ -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) {
|
||||
|
@ -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,9 +175,9 @@ this.AboutServiceWorkers = {
|
||||
Ci.nsIServiceWorkerUnregisterCallback
|
||||
])
|
||||
};
|
||||
gServiceWorkerManager.unregister(principal,
|
||||
serviceWorkerUnregisterCallback,
|
||||
message.scope);
|
||||
gServiceWorkerManager.propagateUnregister(principal,
|
||||
serviceWorkerUnregisterCallback,
|
||||
message.scope);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
|
@ -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" >
|
||||
|
@ -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)) {
|
||||
this._windowUtils.remoteFrameFullscreenChanged(browser);
|
||||
}
|
||||
this.enterDomFullscreen(browser);
|
||||
case "DOMFullscreen:Request": {
|
||||
this._windowUtils.remoteFrameFullscreenChanged(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";
|
||||
},
|
||||
|
@ -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;"/>
|
||||
|
@ -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;
|
||||
},
|
||||
|
@ -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
|
@ -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
|
@ -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 のマイリストを表示
|
@ -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
|
@ -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">
|
@ -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;
|
||||
}
|
||||
|
@ -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;
|
||||
|
@ -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
|
||||
});
|
||||
}
|
||||
|
@ -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);
|
||||
|
@ -62,12 +62,22 @@
|
||||
</popupnotificationcontent>
|
||||
</popupnotification>
|
||||
|
||||
<vbox id="login-fill-doorhanger" hidden="true">
|
||||
<description id="login-fill-testing"
|
||||
value="Thanks for testing the login fill doorhanger!"/>
|
||||
<textbox id="login-fill-filter"/>
|
||||
<richlistbox id="login-fill-list"/>
|
||||
</vbox>
|
||||
<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">
|
||||
|
@ -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) {
|
||||
|
@ -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": {
|
||||
|
@ -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;
|
||||
|
@ -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", () => {
|
||||
|
@ -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();
|
||||
|
||||
|
@ -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;
|
||||
},
|
||||
|
||||
|
@ -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");
|
||||
|
||||
|
@ -26,7 +26,7 @@ function frame_script() {
|
||||
*/
|
||||
function prepareNonRemoteBrowser(aWindow, browser) {
|
||||
browser.loadURI(NON_REMOTE_PAGE);
|
||||
return waitForDocLoadComplete(browser);
|
||||
return BrowserTestUtils.browserLoaded(browser);
|
||||
}
|
||||
|
||||
registerCleanupFunction(() => {
|
||||
|
@ -633,21 +633,33 @@ 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);
|
||||
if (win.location.href !== url) {
|
||||
info("ignoring a window with this url: " + win.location.href);
|
||||
return;
|
||||
}
|
||||
win.addEventListener("load", function loadHandler() {
|
||||
win.removeEventListener("load", loadHandler);
|
||||
|
||||
Services.obs.removeObserver(obs, "domwindowopened");
|
||||
resolve(win);
|
||||
if (win.location.href !== url) {
|
||||
info("ignoring a window with this url: " + win.location.href);
|
||||
return;
|
||||
}
|
||||
|
||||
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);
|
||||
|
@ -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;
|
||||
]>
|
||||
|
@ -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)
|
||||
|
@ -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"/>
|
||||
|
@ -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);
|
||||
|
@ -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) {
|
||||
|
@ -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;
|
||||
}
|
||||
|
@ -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. */
|
||||
|
@ -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({
|
||||
|
@ -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({
|
||||
|
@ -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}
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -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}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -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
|
||||
|
@ -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) {
|
||||
this.dispatchAction(new sharedActions.UpdateRoomContextDone());
|
||||
// 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;
|
||||
}
|
||||
|
||||
|
@ -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);
|
||||
}
|
||||
@ -345,7 +352,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
|
||||
getDefaultProps: function() {
|
||||
return { editMode: false };
|
||||
},
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var url = this._getURL();
|
||||
@ -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,
|
||||
|
@ -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);
|
||||
}
|
||||
@ -345,7 +352,7 @@ loop.roomViews = (function(mozL10n) {
|
||||
|
||||
getDefaultProps: function() {
|
||||
return { editMode: false };
|
||||
},
|
||||
},
|
||||
|
||||
getInitialState: function() {
|
||||
var url = this._getURL();
|
||||
@ -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}
|
||||
|
@ -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)
|
||||
|
@ -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;
|
||||
}
|
||||
|
||||
|
@ -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;
|
||||
|
||||
/*
|
||||
* Expand to fill the available space, since there is no video any
|
||||
* intrinsic width. XXX should really change to an <img> for clarity
|
||||
*/
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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.
|
||||
*/
|
||||
.local-stream-audio .OT_publisher .OT_video-poster {
|
||||
opacity: 1
|
||||
.local .avatar {
|
||||
position: absolute;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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;
|
||||
}
|
||||
|
||||
/*
|
||||
* 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%;
|
||||
}
|
||||
|
@ -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)
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
|
@ -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;
|
||||
},
|
||||
|
||||
/**
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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
|
||||
};
|
||||
|
@ -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);
|
||||
|
@ -131,7 +131,6 @@ loop.StandaloneMozLoop = (function(mozL10n) {
|
||||
},
|
||||
async: async,
|
||||
success: function(responseData) {
|
||||
console.log("done");
|
||||
try {
|
||||
callback(null, validate(responseData, expectedProps));
|
||||
} catch (err) {
|
||||
|
@ -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: 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("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,
|
||||
|
@ -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}
|
||||
|
@ -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);
|
||||
|
@ -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");
|
||||
});
|
||||
});
|
||||
|
@ -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);
|
||||
|
@ -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() {
|
||||
|
@ -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()
|
||||
|
@ -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() {
|
||||
|
@ -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() {
|
||||
|
@ -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 = {
|
||||
|
@ -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() {
|
||||
|
@ -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});
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@ -178,24 +178,16 @@ describe("loop.standaloneRoomViews", function() {
|
||||
return TestUtils.renderIntoDocument(
|
||||
React.createElement(
|
||||
loop.standaloneRoomViews.StandaloneRoomView, {
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
isFirefox: true
|
||||
}));
|
||||
dispatcher: dispatcher,
|
||||
activeRoomStore: activeRoomStore,
|
||||
isFirefox: true
|
||||
}));
|
||||
}
|
||||
|
||||
function expectActionDispatched(view) {
|
||||
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() {
|
||||
|
@ -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>
|
||||
|
138
browser/components/loop/ui/react-frame-component.js
vendored
Normal file
138
browser/components/loop/ui/react-frame-component.js
vendored
Normal 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 |
BIN
browser/components/loop/ui/sample-img/video-screen-terminal.png
Normal file
BIN
browser/components/loop/ui/sample-img/video-screen-terminal.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 263 KiB |
@ -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 {
|
||||
|
@ -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, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
/**
|
||||
* 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(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(Example, {summary: "Desktop conversation window large", dashed: "true"},
|
||||
React.createElement("div", {className: "breakpoint", "data-breakpoint-width": "800px",
|
||||
"data-breakpoint-height": "600px"},
|
||||
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},
|
||||
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: 298, height: 254,
|
||||
summary: "Desktop ongoing conversation window - local face mute"},
|
||||
React.createElement("div", {className: "fx-embedded"},
|
||||
React.createElement(ConversationView, {sdk: mockSDK,
|
||||
video: {enabled: false},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
React.createElement(OngoingConversationView, {
|
||||
dispatcher: dispatcher,
|
||||
video: {enabled: false},
|
||||
audio: {enabled: true},
|
||||
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,
|
||||
video: {enabled: true},
|
||||
audio: {enabled: true},
|
||||
model: mockConversationModel})
|
||||
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},
|
||||
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: 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: activeRoomStore,
|
||||
roomState: ROOM_STATES.HAS_PARTICIPANTS,
|
||||
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,23 +951,28 @@
|
||||
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";
|
||||
|
||||
// Put the title back, in case views changed it.
|
||||
document.title = "Loop UI Components Showcase";
|
||||
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
if (uncaughtError) {
|
||||
$("#results").append("<div class='failures'><em>1</em></div>");
|
||||
$("#results").append("<li class='test fail'>" +
|
||||
"<h2>Errors rendering UI-Showcase</h2>" +
|
||||
"<pre class='error'>" + uncaughtError + "\n" + uncaughtError.stack + "</pre>" +
|
||||
"</li>");
|
||||
} else {
|
||||
$("#results").append("<div class='failures'><em>0</em></div>");
|
||||
}
|
||||
$("#results").append("<p id='complete'>Complete.</p>");
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
if (uncaughtError) {
|
||||
$("#results").append("<div class='failures'><em>1</em></div>");
|
||||
$("#results").append("<li class='test fail'>" +
|
||||
"<h2>Errors rendering UI-Showcase</h2>" +
|
||||
"<pre class='error'>" + uncaughtError + "\n" + uncaughtError.stack + "</pre>" +
|
||||
"</li>");
|
||||
} else {
|
||||
$("#results").append("<div class='failures'><em>0</em></div>");
|
||||
}
|
||||
$("#results").append("<p id='complete'>Complete.</p>");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -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, {
|
||||
mozLoop: navigator.mozLoop,
|
||||
sdkDriver: mockSDK
|
||||
/**
|
||||
* 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("#")}> ¶</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}} />
|
||||
<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>
|
||||
</Example>
|
||||
</FramedExample>
|
||||
|
||||
<Example summary="Desktop conversation window large" dashed="true">
|
||||
<div className="breakpoint" data-breakpoint-width="800px"
|
||||
data-breakpoint-height="600px">
|
||||
<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}}
|
||||
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={298} height={254}
|
||||
summary="Desktop ongoing conversation window - local face mute">
|
||||
<div className="fx-embedded">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: false}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
<OngoingConversationView
|
||||
dispatcher={dispatcher}
|
||||
video={{enabled: false}}
|
||||
audio={{enabled: true}}
|
||||
remoteVideoEnabled={true}
|
||||
remotePosterUrl="sample-img/video-screen-remote.png"
|
||||
mediaConnected={true} />
|
||||
</div>
|
||||
</Example>
|
||||
</FramedExample>
|
||||
|
||||
<Example summary="Standalone version">
|
||||
<div className="standalone">
|
||||
<ConversationView sdk={mockSDK}
|
||||
video={{enabled: true}}
|
||||
audio={{enabled: true}}
|
||||
model={mockConversationModel} />
|
||||
<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}}
|
||||
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={updatingActiveRoomStore}
|
||||
roomState={ROOM_STATES.HAS_PARTICIPANTS}
|
||||
isFirefox={true}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
remotePosterUrl="sample-img/video-screen-remote.png" />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<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.HAS_PARTICIPANTS}
|
||||
activeRoomStore={localFaceMuteRoomStore}
|
||||
isFirefox={true}
|
||||
localPosterUrl="sample-img/video-screen-local.png"
|
||||
remotePosterUrl="sample-img/video-screen-remote.png" />
|
||||
</div>
|
||||
</FramedExample>
|
||||
|
||||
<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={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>
|
||||
</Example>
|
||||
</FramedExample>
|
||||
|
||||
<Example summary="Standalone room conversation (full - FFx user)">
|
||||
<FramedExample width={644} height={483}
|
||||
summary="Standalone room conversation (full - non FFx user)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FULL}
|
||||
isFirefox={true} />
|
||||
</div>
|
||||
</Example>
|
||||
|
||||
<Example summary="Standalone room conversation (full - non FFx user)">
|
||||
<div className="standalone">
|
||||
<StandaloneRoomView
|
||||
dispatcher={dispatcher}
|
||||
activeRoomStore={activeRoomStore}
|
||||
roomState={ROOM_STATES.FULL}
|
||||
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,23 +951,28 @@
|
||||
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";
|
||||
|
||||
// Put the title back, in case views changed it.
|
||||
document.title = "Loop UI Components Showcase";
|
||||
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
if (uncaughtError) {
|
||||
$("#results").append("<div class='failures'><em>1</em></div>");
|
||||
$("#results").append("<li class='test fail'>" +
|
||||
"<h2>Errors rendering UI-Showcase</h2>" +
|
||||
"<pre class='error'>" + uncaughtError + "\n" + uncaughtError.stack + "</pre>" +
|
||||
"</li>");
|
||||
} else {
|
||||
$("#results").append("<div class='failures'><em>0</em></div>");
|
||||
}
|
||||
$("#results").append("<p id='complete'>Complete.</p>");
|
||||
// This simulates the mocha layout for errors which means we can run
|
||||
// this alongside our other unit tests but use the same harness.
|
||||
if (uncaughtError) {
|
||||
$("#results").append("<div class='failures'><em>1</em></div>");
|
||||
$("#results").append("<li class='test fail'>" +
|
||||
"<h2>Errors rendering UI-Showcase</h2>" +
|
||||
"<pre class='error'>" + uncaughtError + "\n" + uncaughtError.stack + "</pre>" +
|
||||
"</li>");
|
||||
} else {
|
||||
$("#results").append("<div class='failures'><em>0</em></div>");
|
||||
}
|
||||
$("#results").append("<p id='complete'>Complete.</p>");
|
||||
}, 1000);
|
||||
});
|
||||
|
||||
})();
|
||||
|
@ -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() {
|
||||
|
@ -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)
|
||||
|
@ -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 -- //
|
||||
|
@ -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. It’s 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: "Просмотреть список"
|
||||
};
|
@ -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();
|
||||
}
|
||||
|
||||
window.thePKT_SAVED.create();
|
||||
// send an async message to get string data
|
||||
thePKT_SAVED.sendMessage("initL10N", {}, function(resp) {
|
||||
window.pocketStrings = resp.strings;
|
||||
window.thePKT_SAVED.create();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
window.thePKT_SIGNUP.create();
|
||||
// send an async message to get string data
|
||||
thePKT_SIGNUP.sendMessage("initL10N", {}, function(resp) {
|
||||
window.pocketStrings = resp.strings;
|
||||
window.thePKT_SIGNUP.create();
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -10,9 +10,8 @@
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -9,9 +9,8 @@
|
||||
<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>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
|
@ -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);
|
||||
}
|
||||
};
|
||||
|
||||
/**
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
|
@ -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.
|
||||
|
@ -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);
|
||||
|
||||
|
@ -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));
|
||||
|
@ -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);
|
||||
|
@ -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.
|
||||
|
@ -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];
|
||||
}
|
||||
|
@ -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;
|
||||
}),
|
||||
|
||||
/**
|
||||
|
@ -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.
|
||||
*/
|
||||
|
@ -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;"
|
||||
|
@ -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");
|
||||
|
@ -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>
|
||||
|
476
browser/devtools/netmonitor/har/har-builder.js
Normal file
476
browser/devtools/netmonitor/har/har-builder.js
Normal 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;
|
175
browser/devtools/netmonitor/har/har-exporter.js
Normal file
175
browser/devtools/netmonitor/har/har-exporter.js
Normal 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;
|
185
browser/devtools/netmonitor/har/har-utils.js
Normal file
185
browser/devtools/netmonitor/har/har-utils.js
Normal 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
Loading…
Reference in New Issue
Block a user