diff --git a/CLOBBER b/CLOBBER index 1b0ac3bd9e3..5c929901aae 100644 --- a/CLOBBER +++ b/CLOBBER @@ -22,4 +22,4 @@ # changes to stick? As of bug 928195, this shouldn't be necessary! Please # don't change CLOBBER for WebIDL changes any more. -Bug 1101553 - remove nsPIPlacesHistoryListenersNotifier +Bug 1118618's backout needed a clobber diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 14f6ac7ce3e..8f37f95a3db 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1430,7 +1430,9 @@ pref("devtools.timeline.hiddenMarkers", "[]"); pref("devtools.performance.ui.show-timeline-memory", false); // The default Profiler UI settings +pref("devtools.profiler.ui.flatten-tree-recursion", true); pref("devtools.profiler.ui.show-platform-data", false); +pref("devtools.profiler.ui.show-idle-blocks", true); // The default cache UI setting pref("devtools.cache.disabled", false); @@ -1675,7 +1677,6 @@ pref("loop.CSP", "default-src 'self' about: file: chrome:; img-src 'self' data: #endif pref("loop.oauth.google.redirect_uri", "urn:ietf:wg:oauth:2.0:oob:auto"); pref("loop.oauth.google.scope", "https://www.google.com/m8/feeds"); -pref("loop.rooms.enabled", true); pref("loop.fxa_oauth.tokendata", ""); pref("loop.fxa_oauth.profile", ""); pref("loop.support_url", "https://support.mozilla.org/kb/group-conversations-firefox-hello-webrtc"); diff --git a/browser/base/content/aboutDialog.js b/browser/base/content/aboutDialog.js index cbc1fcac518..e2d40db15b4 100644 --- a/browser/base/content/aboutDialog.js +++ b/browser/base/content/aboutDialog.js @@ -490,7 +490,7 @@ appUpdater.prototype = return; } - this.selectPanel("apply"); + this.selectPanel("applyBillboard"); }, /** diff --git a/browser/base/content/browser-ctrlTab.js b/browser/base/content/browser-ctrlTab.js index 8191cb6873f..c2b6a315604 100644 --- a/browser/base/content/browser-ctrlTab.js +++ b/browser/base/content/browser-ctrlTab.js @@ -399,9 +399,9 @@ var ctrlTab = { suspendGUI: function ctrlTab_suspendGUI() { document.removeEventListener("keyup", this, true); - Array.forEach(this.previews, function (preview) { + for (let preview of this.previews) { this.updatePreview(preview, null); - }, this); + } }, onKeyPress: function ctrlTab_onKeyPress(event) { diff --git a/browser/base/content/test/general/browser.ini b/browser/base/content/test/general/browser.ini index f453b09d9aa..856b56ada78 100644 --- a/browser/base/content/test/general/browser.ini +++ b/browser/base/content/test/general/browser.ini @@ -303,6 +303,8 @@ run-if = datareporting skip-if = buildapp == 'mulet' || (os == "linux" && debug) || e10s # linux: bug 976544; e10s: bug 1071623 [browser_devices_get_user_media_about_urls.js] skip-if = e10s # Bug 1071623 +[browser_devices_get_user_media_in_frame.js] +skip-if = e10s # Bug 1071623 [browser_discovery.js] [browser_double_close_tab.js] skip-if = e10s diff --git a/browser/base/content/test/general/browser_devices_get_user_media.js b/browser/base/content/test/general/browser_devices_get_user_media.js index 6473144cadf..19befedb250 100644 --- a/browser/base/content/test/general/browser_devices_get_user_media.js +++ b/browser/base/content/test/general/browser_devices_get_user_media.js @@ -459,6 +459,50 @@ let gTests = [ } }, +{ + desc: "getUserMedia audio+video: reloading the page removes all gUM UI", + run: function checkReloading() { + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting devices"); + content.wrappedJSObject.requestDevice(true, true); + }); + expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectObserverCalled("getUserMedia:response:allow"); + expectObserverCalled("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI({video: true, audio: true}); + + yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices")); + + info("reloading the web page"); + let deferred = Promise.defer(); + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + deferred.resolve(); + }, true); + content.location.reload(); + yield deferred.promise; + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + if (gObservedTopics["recording-device-events"] == 2) { + todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719"); + --gObservedTopics["recording-device-events"]; + } + expectObserverCalled("recording-device-events"); + expectObserverCalled("recording-window-ended"); + expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + { desc: "getUserMedia prompt: Always/Never Share", run: function checkRememberCheckbox() { diff --git a/browser/base/content/test/general/browser_devices_get_user_media_in_frame.js b/browser/base/content/test/general/browser_devices_get_user_media_in_frame.js new file mode 100644 index 00000000000..72582d6451b --- /dev/null +++ b/browser/base/content/test/general/browser_devices_get_user_media_in_frame.js @@ -0,0 +1,477 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +const kObservedTopics = [ + "getUserMedia:response:allow", + "getUserMedia:revoke", + "getUserMedia:response:deny", + "getUserMedia:request", + "recording-device-events", + "recording-window-ended" +]; + +const PREF_PERMISSION_FAKE = "media.navigator.permission.fake"; + +Cu.import("resource://gre/modules/XPCOMUtils.jsm"); +XPCOMUtils.defineLazyServiceGetter(this, "MediaManagerService", + "@mozilla.org/mediaManagerService;1", + "nsIMediaManagerService"); + +var gObservedTopics = {}; +function observer(aSubject, aTopic, aData) { + if (!(aTopic in gObservedTopics)) + gObservedTopics[aTopic] = 1; + else + ++gObservedTopics[aTopic]; +} + +function promiseObserverCalled(aTopic, aAction) { + let deferred = Promise.defer(); + + Services.obs.addObserver(function observer() { + ok(true, "got " + aTopic + " notification"); + Services.obs.removeObserver(observer, aTopic); + + if (kObservedTopics.indexOf(aTopic) != -1) { + if (!(aTopic in gObservedTopics)) + gObservedTopics[aTopic] = -1; + else + --gObservedTopics[aTopic]; + } + + deferred.resolve(); + }, aTopic, false); + + if (aAction) + aAction(); + + return deferred.promise; +} + +function expectObserverCalled(aTopic) { + is(gObservedTopics[aTopic], 1, "expected notification " + aTopic); + if (aTopic in gObservedTopics) + --gObservedTopics[aTopic]; +} + +function expectNoObserverCalled() { + for (let topic in gObservedTopics) { + if (gObservedTopics[topic]) + is(gObservedTopics[topic], 0, topic + " notification unexpected"); + } + gObservedTopics = {}; +} + +function promiseMessage(aMessage, aAction) { + let deferred = Promise.defer(); + + content.addEventListener("message", function messageListener(event) { + content.removeEventListener("message", messageListener); + is(event.data, aMessage, "received " + aMessage); + if (event.data == aMessage) + deferred.resolve(); + else + deferred.reject(); + }); + + if (aAction) + aAction(); + + return deferred.promise; +} + +function promisePopupNotificationShown(aName, aAction) { + let deferred = Promise.defer(); + + PopupNotifications.panel.addEventListener("popupshown", function popupNotifShown() { + PopupNotifications.panel.removeEventListener("popupshown", popupNotifShown); + + ok(!!PopupNotifications.getNotification(aName), aName + " notification shown"); + ok(PopupNotifications.isPanelOpen, "notification panel open"); + ok(!!PopupNotifications.panel.firstChild, "notification panel populated"); + + deferred.resolve(); + }); + + if (aAction) + aAction(); + + return deferred.promise; +} + +function promisePopupNotification(aName) { + let deferred = Promise.defer(); + + waitForCondition(() => PopupNotifications.getNotification(aName), + () => { + ok(!!PopupNotifications.getNotification(aName), + aName + " notification appeared"); + + deferred.resolve(); + }, "timeout waiting for popup notification " + aName); + + return deferred.promise; +} + +function promiseNoPopupNotification(aName) { + let deferred = Promise.defer(); + + waitForCondition(() => !PopupNotifications.getNotification(aName), + () => { + ok(!PopupNotifications.getNotification(aName), + aName + " notification removed"); + deferred.resolve(); + }, "timeout waiting for popup notification " + aName + " to disappear"); + + return deferred.promise; +} + +const kActionAlways = 1; +const kActionDeny = 2; +const kActionNever = 3; + +function activateSecondaryAction(aAction) { + let notification = PopupNotifications.panel.firstChild; + notification.button.focus(); + let popup = notification.menupopup; + popup.addEventListener("popupshown", function () { + popup.removeEventListener("popupshown", arguments.callee, false); + + // Press 'down' as many time as needed to select the requested action. + while (aAction--) + EventUtils.synthesizeKey("VK_DOWN", {}); + + // Activate + EventUtils.synthesizeKey("VK_RETURN", {}); + }, false); + + // One down event to open the popup + EventUtils.synthesizeKey("VK_DOWN", + { altKey: !navigator.platform.contains("Mac") }); +} + +registerCleanupFunction(function() { + gBrowser.removeCurrentTab(); + kObservedTopics.forEach(topic => { + Services.obs.removeObserver(observer, topic); + }); + Services.prefs.clearUserPref(PREF_PERMISSION_FAKE); +}); + +function getMediaCaptureState() { + let hasVideo = {}; + let hasAudio = {}; + MediaManagerService.mediaCaptureWindowState(content, hasVideo, hasAudio); + if (hasVideo.value && hasAudio.value) + return "CameraAndMicrophone"; + if (hasVideo.value) + return "Camera"; + if (hasAudio.value) + return "Microphone"; + return "none"; +} + +function* closeStream(aGlobal, aAlreadyClosed) { + expectNoObserverCalled(); + + info("closing the stream"); + aGlobal.closeStream(); + + if (!aAlreadyClosed) + yield promiseObserverCalled("recording-device-events"); + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + if (!aAlreadyClosed) + expectObserverCalled("recording-window-ended"); + + yield* assertWebRTCIndicatorStatus(null); +} + +function checkDeviceSelectors(aAudio, aVideo) { + let micSelector = document.getElementById("webRTC-selectMicrophone"); + if (aAudio) + ok(!micSelector.hidden, "microphone selector visible"); + else + ok(micSelector.hidden, "microphone selector hidden"); + + let cameraSelector = document.getElementById("webRTC-selectCamera"); + if (aVideo) + ok(!cameraSelector.hidden, "camera selector visible"); + else + ok(cameraSelector.hidden, "camera selector hidden"); +} + +function* checkSharingUI(aExpected) { + yield promisePopupNotification("webRTC-sharingDevices"); + + yield* assertWebRTCIndicatorStatus(aExpected); +} + +function* checkNotSharing() { + is(getMediaCaptureState(), "none", "expected nothing to be shared"); + + ok(!PopupNotifications.getNotification("webRTC-sharingDevices"), + "no webRTC-sharingDevices popup notification"); + + yield* assertWebRTCIndicatorStatus(null); +} + +function getFrameGlobal(aFrameId) { + return content.wrappedJSObject.document.getElementById(aFrameId).contentWindow; +} + +const permissionError = "error: PermissionDeniedError: The user did not grant permission for the operation."; + +let gTests = [ + +{ + desc: "getUserMedia audio+video", + run: function checkAudioVideo() { + let global = getFrameGlobal("frame1"); + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting devices"); + global.requestDevice(true, true); + }); + expectObserverCalled("getUserMedia:request"); + + is(PopupNotifications.getNotification("webRTC-shareDevices").anchorID, + "webRTC-shareDevices-notification-icon", "anchored to device icon"); + checkDeviceSelectors(true, true); + is(PopupNotifications.panel.firstChild.getAttribute("popupid"), + "webRTC-shareDevices", "panel using devices icon"); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectObserverCalled("getUserMedia:response:allow"); + expectObserverCalled("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI({audio: true, video: true}); + yield closeStream(global); + } +}, + +{ + desc: "getUserMedia audio+video: stop sharing", + run: function checkStopSharing() { + let global = getFrameGlobal("frame1"); + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting devices"); + global.requestDevice(true, true); + }); + expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectObserverCalled("getUserMedia:response:allow"); + expectObserverCalled("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI({video: true, audio: true}); + + yield promiseNotificationShown(PopupNotifications.getNotification("webRTC-sharingDevices")); + activateSecondaryAction(kActionDeny); + + yield promiseObserverCalled("recording-device-events"); + expectObserverCalled("getUserMedia:revoke"); + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + expectObserverCalled("recording-window-ended"); + + if (gObservedTopics["recording-device-events"] == 1) { + todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719"); + gObservedTopics["recording-device-events"] = 0; + } + + expectNoObserverCalled(); + yield checkNotSharing(); + + // the stream is already closed, but this will do some cleanup anyway + yield closeStream(global, true); + } +}, + +{ + desc: "getUserMedia audio+video: reloading the frame removes all sharing UI", + run: function checkReloading() { + let global = getFrameGlobal("frame1"); + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting devices"); + global.requestDevice(true, true); + }); + expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectObserverCalled("getUserMedia:response:allow"); + expectObserverCalled("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI({video: true, audio: true}); + + info("reloading the frame"); + let deferred = Promise.defer(); + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + deferred.resolve(); + }, true); + global.location.reload(); + yield deferred.promise; + + yield promiseNoPopupNotification("webRTC-sharingDevices"); + if (gObservedTopics["recording-device-events"] == 2) { + todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719"); + --gObservedTopics["recording-device-events"]; + } + expectObserverCalled("recording-device-events"); + expectObserverCalled("recording-window-ended"); + expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: reloading the frame removes prompts", + run: function checkReloadingRemovesPrompts() { + let global = getFrameGlobal("frame1"); + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting devices"); + global.requestDevice(true, true); + }); + expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + info("reloading the frame"); + let deferred = Promise.defer(); + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + deferred.resolve(); + }, true); + global.location.reload(); + yield deferred.promise; + + yield promiseNoPopupNotification("webRTC-shareDevices"); + + expectObserverCalled("recording-window-ended"); + expectNoObserverCalled(); + yield checkNotSharing(); + } +}, + +{ + desc: "getUserMedia audio+video: reloading a frame updates the sharing UI", + run: function checkUpdateWhenReloading() { + // We'll share only the mic in the first frame, then share both in the + // second frame, then reload the second frame. After each step, we'll check + // the UI is in the correct state. + let g1 = getFrameGlobal("frame1"), g2 = getFrameGlobal("frame2"); + + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting microphone in the first frame"); + g1.requestDevice(true, false); + }); + expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, false); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectObserverCalled("getUserMedia:response:allow"); + expectObserverCalled("recording-device-events"); + is(getMediaCaptureState(), "Microphone", "microphone to be shared"); + + yield checkSharingUI({video: false, audio: true}); + expectNoObserverCalled(); + + yield promisePopupNotificationShown("webRTC-shareDevices", () => { + info("requesting both devices in the second frame"); + g2.requestDevice(true, true); + }); + expectObserverCalled("getUserMedia:request"); + checkDeviceSelectors(true, true); + + yield promiseMessage("ok", () => { + PopupNotifications.panel.firstChild.button.click(); + }); + expectObserverCalled("getUserMedia:response:allow"); + expectObserverCalled("recording-device-events"); + is(getMediaCaptureState(), "CameraAndMicrophone", + "expected camera and microphone to be shared"); + + yield checkSharingUI({video: true, audio: true}); + expectNoObserverCalled(); + + info("reloading the second frame"); + let deferred = Promise.defer(); + let browser = gBrowser.selectedBrowser; + browser.addEventListener("load", function onload() { + browser.removeEventListener("load", onload, true); + deferred.resolve(); + }, true); + g2.location.reload(); + yield deferred.promise; + + yield checkSharingUI({video: false, audio: true}); + expectObserverCalled("recording-window-ended"); + if (gObservedTopics["recording-device-events"] == 2) { + todo(false, "Got the 'recording-device-events' notification twice, likely because of bug 962719"); + --gObservedTopics["recording-device-events"]; + } + expectObserverCalled("recording-device-events"); + expectNoObserverCalled(); + + yield closeStream(g1); + yield promiseNoPopupNotification("webRTC-sharingDevices"); + expectNoObserverCalled(); + yield checkNotSharing(); + } +} + +]; + +function test() { + waitForExplicitFinish(); + + let tab = gBrowser.addTab(); + gBrowser.selectedTab = tab; + tab.linkedBrowser.addEventListener("load", function onload() { + tab.linkedBrowser.removeEventListener("load", onload, true); + + kObservedTopics.forEach(topic => { + Services.obs.addObserver(observer, topic, false); + }); + Services.prefs.setBoolPref(PREF_PERMISSION_FAKE, true); + + is(PopupNotifications._currentNotifications.length, 0, + "should start the test without any prior popup notification"); + + Task.spawn(function () { + for (let test of gTests) { + info(test.desc); + yield test.run(); + + // Cleanup before the next test + expectNoObserverCalled(); + } + }).then(finish, ex => { + ok(false, "Unexpected Exception: " + ex); + finish(); + }); + }, true); + let rootDir = getRootDirectory(gTestPath); + rootDir = rootDir.replace("chrome://mochitests/content/", + "https://example.com/"); + let url = rootDir + "get_user_media.html"; + content.location = 'data:text/html,' +} diff --git a/browser/base/content/test/general/pinning_reports.sjs b/browser/base/content/test/general/pinning_reports.sjs index 16cf98d08f0..fde7d16ae4d 100644 --- a/browser/base/content/test/general/pinning_reports.sjs +++ b/browser/base/content/test/general/pinning_reports.sjs @@ -3,6 +3,8 @@ const EXPECTED_CHAIN = [ "MIIC2jCCAcKgAwIBAgIBATANBgkqhkiG9w0BAQsFADAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwHhcNMTQwOTI1MjEyMTU0WhcNMjQwOTI1MjEyMTU0WjAmMSQwIgYDVQQDExtBbHRlcm5hdGUgVHJ1c3RlZCBBdXRob3JpdHkwggEiMA0GCSqGSIb3DQEBAQUAA4IBDwAwggEKAoIBAQDBT+BwAhO52IWgSIdZZifU9LHOs3IR/+8DCC0WP5d/OuyKlZ6Rqd0tsd3i7durhQyjHSbLf2lJStcnFjcVEbEnNI76RuvlN8xLLn5eV+2Ayr4cZYKztudwRmw+DV/iYAiMSy0hs7m3ssfX7qpoi1aNRjUanwU0VTCPQhF1bEKAC2du+C5Z8e92zN5t87w7bYr7lt+m8197XliXEu+0s9RgnGwGaZ296BIRz6NOoJYTa43n06LU1I1+Z4d6lPdzUFrSR0GBaMhUSurUBtOin3yWiMhg1VHX/KwqGc4als5GyCVXy8HGrA/0zQPOhetxrlhEVAdK/xBt7CZvByj1Rcc7AgMBAAGjEzARMA8GA1UdEwQIMAYBAf8CAQAwDQYJKoZIhvcNAQELBQADggEBAJq/hogSRqzPWTwX4wTn/DVSNdWwFLv53qep9YrSMJ8ZsfbfK9Es4VP4dBLRQAVMJ0Z5mW1I6d/n0KayTanuUBvemYdxPi/qQNSs8UJcllqdhqWzmzAg6a0LxrMnEeKzPBPD6q8PwQ7tYP+B4sBN9tnnsnyPgti9ZiNZn5FwXZliHXseQ7FE9/SqHlLw5LXW3YtKjuti6RmuV6fq3j+D4oeC5vb1mKgIyoTqGN6ze57v8RHi+pQ8Q+kmoUn/L3Z2YmFe4SKN/4WoyXr8TdejpThGOCGCAd3565s5gOx5QfSQX11P8NZKO8hcN0tme3VzmGpHK0Z/6MTmdpNaTwQ6odk=" ]; +const MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE = -16384; + function parseReport(request) { // read the report from the request let inputStream = Components.classes["@mozilla.org/scriptableinputstream;1"].createInstance(Components.interfaces.nsIScriptableInputStream); @@ -38,6 +40,12 @@ function handleRequest(request, response) { } } + if (report.errorCode !== MOZILLA_PKIX_ERROR_KEY_PINNING_FAILURE) { + response.setStatusLine("1.1", 500, "Server error"); + response.write("The report contained an unexpected error code"); + return; + } + // if all is as expected, send the 201 the client expects response.setStatusLine("1.1", 201, "Created"); response.write("OK"); diff --git a/browser/base/content/urlbarBindings.xml b/browser/base/content/urlbarBindings.xml index 4dcba14d572..0e9caeafc1c 100644 --- a/browser/base/content/urlbarBindings.xml +++ b/browser/base/content/urlbarBindings.xml @@ -334,14 +334,16 @@ Cu.reportError(ex); } - function loadCurrent() { + let loadCurrent = () => { openUILinkIn(url, "current", { allowThirdPartyFixup: true, disallowInheritPrincipal: !mayInheritPrincipal, allowPinnedTabHostChange: true, postData: postData }); - } + // Ensure the start of the URL is visible for UX reasons: + this.selectionStart = this.selectionEnd = 0; + }; // Focus the content area before triggering loads, since if the load // occurs in a new tab, we want focus to be restored to the content @@ -1149,7 +1151,8 @@ let header = document.getAnonymousElementByAttribute(this, "anonid", "search-panel-one-offs-header") - header.collapsed = list.collapsed = !engines.length; + // header is a xul:deck so collapsed doesn't work on it, see bug 589569. + header.hidden = list.collapsed = !engines.length; // 49px is the min-width of each search engine button, // adapt this const when changing the css. diff --git a/browser/components/downloads/test/browser/browser.ini b/browser/components/downloads/test/browser/browser.ini index c7f1fd16005..d9cb4e58c2d 100644 --- a/browser/components/downloads/test/browser/browser.ini +++ b/browser/components/downloads/test/browser/browser.ini @@ -8,3 +8,7 @@ skip-if = os == "linux" # Bug 949434 [browser_overflow_anchor.js] skip-if = os == "linux" # Bug 952422 [browser_confirm_unblock_download.js] + +[browser_iframe_gone_mid_download.js] +skip-if = e10s + diff --git a/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js b/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js new file mode 100644 index 00000000000..ebdd4f9af84 --- /dev/null +++ b/browser/components/downloads/test/browser/browser_iframe_gone_mid_download.js @@ -0,0 +1,62 @@ +const SAVE_PER_SITE_PREF = "browser.download.lastDir.savePerSite"; + +function test_deleted_iframe(perSitePref, windowOptions={}) { + return function*() { + Services.prefs.setBoolPref(SAVE_PER_SITE_PREF, perSitePref); + let {DownloadLastDir} = Cu.import("resource://gre/modules/DownloadLastDir.jsm", {}); + + let win = yield promiseOpenAndLoadWindow(windowOptions); + let tab = win.gBrowser.addTab(); + yield promiseTabLoadEvent(tab, "about:mozilla"); + + let doc = tab.linkedBrowser.contentDocument; + let iframe = doc.createElement("iframe"); + doc.body.appendChild(iframe); + + ok(iframe.contentWindow, "iframe should have a window"); + let gDownloadLastDir = new DownloadLastDir(iframe.contentWindow); + let cw = iframe.contentWindow; + let promiseIframeWindowGone = new Promise((resolve, reject) => { + Services.obs.addObserver(function obs(subject, topic) { + if (subject == cw) { + Services.obs.removeObserver(obs, topic); + resolve(); + } + }, "dom-window-destroyed", false); + }); + iframe.remove(); + yield promiseIframeWindowGone; + cw = null; + ok(!iframe.contentWindow, "Managed to destroy iframe"); + + let someDir = "blah"; + try { + someDir = yield new Promise((resolve, reject) => { + gDownloadLastDir.getFileAsync("http://www.mozilla.org/", function(dir) { + resolve(dir); + }); + }); + } catch (ex) { + ok(false, "Got an exception trying to get the directory where things should be saved."); + Cu.reportError(ex); + } + // NB: someDir can legitimately be null here when set, hence the 'blah' workaround: + isnot(someDir, "blah", "Should get a file even after the window was destroyed."); + + try { + gDownloadLastDir.setFile("http://www.mozilla.org/", null); + } catch (ex) { + ok(false, "Got an exception trying to set the directory where things should be saved."); + Cu.reportError(ex); + } + + yield promiseWindowClosed(win); + Services.prefs.clearUserPref(SAVE_PER_SITE_PREF); + }; +} + +add_task(test_deleted_iframe(false)); +add_task(test_deleted_iframe(false)); +add_task(test_deleted_iframe(true, {private: true})); +add_task(test_deleted_iframe(true, {private: true})); + diff --git a/browser/components/downloads/test/browser/head.js b/browser/components/downloads/test/browser/head.js index 6566e693a02..7c4ffe5a342 100644 --- a/browser/components/downloads/test/browser/head.js +++ b/browser/components/downloads/test/browser/head.js @@ -31,6 +31,76 @@ registerCleanupFunction(function () { //////////////////////////////////////////////////////////////////////////////// //// Asynchronous support subroutines +function promiseOpenAndLoadWindow(aOptions) +{ + return new Promise((resolve, reject) => { + let win = OpenBrowserWindow(aOptions); + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + resolve(win); + }); + }); +} + +/** + * Waits for a load (or custom) event to finish in a given tab. If provided + * load an uri into the tab. + * + * @param tab + * The tab to load into. + * @param [optional] url + * The url to load, or the current url. + * @param [optional] event + * The load event type to wait for. Defaults to "load". + * @return {Promise} resolved when the event is handled. + * @resolves to the received event + * @rejects if a valid load event is not received within a meaningful interval + */ +function promiseTabLoadEvent(tab, url, eventType="load") +{ + let deferred = Promise.defer(); + info("Wait tab event: " + eventType); + + function handle(event) { + if (event.originalTarget != tab.linkedBrowser.contentDocument || + event.target.location.href == "about:blank" || + (url && event.target.location.href != url)) { + info("Skipping spurious '" + eventType + "'' event" + + " for " + event.target.location.href); + return; + } + // Remove reference to tab from the cleanup function: + realCleanup = () => {}; + tab.linkedBrowser.removeEventListener(eventType, handle, true); + info("Tab event received: " + eventType); + deferred.resolve(event); + } + + // Juggle a bit to avoid leaks: + let realCleanup = () => tab.linkedBrowser.removeEventListener(eventType, handle, true); + registerCleanupFunction(() => realCleanup()); + + tab.linkedBrowser.addEventListener(eventType, handle, true, true); + if (url) + tab.linkedBrowser.loadURI(url); + return deferred.promise; +} + +function promiseWindowClosed(win) +{ + let promise = new Promise((resolve, reject) => { + Services.obs.addObserver(function obs(subject, topic) { + if (subject == win) { + Services.obs.removeObserver(obs, topic); + resolve(); + } + }, "domwindowclosed", false); + }); + win.close(); + return promise; +} + + function promiseFocus() { let deferred = Promise.defer(); diff --git a/browser/components/loop/MozLoopAPI.jsm b/browser/components/loop/MozLoopAPI.jsm index 3458425e3db..0bea0dc8acc 100644 --- a/browser/components/loop/MozLoopAPI.jsm +++ b/browser/components/loop/MozLoopAPI.jsm @@ -416,26 +416,6 @@ function injectLoopAPI(targetWindow) { } }, - /** - * Used to note a call url expiry time. If the time is later than the current - * latest expiry time, then the stored expiry time is increased. For times - * sooner, this function is a no-op; this ensures we always have the latest - * expiry time for a url. - * - * This is used to determine whether or not we should be registering with the - * push server on start. - * - * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time - * of the url. - */ - noteCallUrlExpiry: { - enumerable: true, - writable: true, - value: function(expiryTimeSeconds) { - MozLoopService.noteCallUrlExpiry(expiryTimeSeconds); - } - }, - /** * Set any preference under "loop." * diff --git a/browser/components/loop/MozLoopService.jsm b/browser/components/loop/MozLoopService.jsm index 2fda6510fde..05ea05a6ca2 100644 --- a/browser/components/loop/MozLoopService.jsm +++ b/browser/components/loop/MozLoopService.jsm @@ -1243,22 +1243,6 @@ this.MozLoopService = { return MozLoopServiceInternal.promiseRegisteredWithServers(sessionType); }, - /** - * Used to note a call url expiry time. If the time is later than the current - * latest expiry time, then the stored expiry time is increased. For times - * sooner, this function is a no-op; this ensures we always have the latest - * expiry time for a url. - * - * This is used to determine whether or not we should be registering with the - * push server on start. - * - * @param {Integer} expiryTimeSeconds The seconds since epoch of the expiry time - * of the url. - */ - noteCallUrlExpiry: function(expiryTimeSeconds) { - MozLoopServiceInternal.expiryTimeSeconds = expiryTimeSeconds; - }, - /** * Returns the strings for the specified element. Designed for use with l10n.js. * diff --git a/browser/components/loop/content/js/client.js b/browser/components/loop/content/js/client.js index 451e435e07f..9b6a0b6e42b 100644 --- a/browser/components/loop/content/js/client.js +++ b/browser/components/loop/content/js/client.js @@ -9,12 +9,6 @@ var loop = loop || {}; loop.Client = (function($) { "use strict"; - // The expected properties to be returned from the POST /call-url/ request. - var expectedCallUrlProperties = ["callUrl", "expiresAt"]; - - // The expected properties to be returned from the GET /calls request. - var expectedCallProperties = ["calls"]; - // THe expected properties to be returned from the POST /calls request. var expectedPostCallProperties = [ "apiKey", "callId", "progressURL", @@ -81,56 +75,6 @@ loop.Client = (function($) { cb(error); }, - /** - * Requests a call URL from the Loop server. It will note the - * expiry time for the url with the mozLoop api. It will select the - * appropriate hawk session to use based on whether or not the user - * is currently logged into a Firefox account profile. - * - * Callback parameters: - * - err null on successful request, non-null otherwise. - * - callUrlData an object of the obtained call url data if successful: - * -- callUrl: The url of the call - * -- expiresAt: The amount of hours until expiry of the url - * - * @param {String} simplepushUrl a registered Simple Push URL - * @param {string} nickname the nickname of the future caller - * @param {Function} cb Callback(err, callUrlData) - */ - requestCallUrl: function(nickname, cb) { - var sessionType; - if (this.mozLoop.userProfile) { - sessionType = this.mozLoop.LOOP_SESSION_TYPE.FXA; - } else { - sessionType = this.mozLoop.LOOP_SESSION_TYPE.GUEST; - } - - this.mozLoop.hawkRequest(sessionType, "/call-url/", "POST", - {callerId: nickname}, - function (error, responseText) { - if (error) { - this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false); - this._failureHandler(cb, error); - return; - } - - try { - var urlData = JSON.parse(responseText); - - // This throws if the data is invalid, in which case only the failure - // telemetry will be recorded. - var returnData = this._validate(urlData, expectedCallUrlProperties); - - this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", true); - cb(null, returnData); - } catch (err) { - this._telemetryAdd("LOOP_CLIENT_CALL_URL_REQUESTS_SUCCESS", false); - console.log("Error requesting call info", err); - cb(err); - } - }.bind(this)); - }, - /** * Block call URL based on the token identifier * @@ -203,20 +147,6 @@ loop.Client = (function($) { }.bind(this) ); }, - - /** - * Adds a value to a telemetry histogram, ignoring errors. - * - * @param {string} histogramId Name of the telemetry histogram to update. - * @param {integer} value Value to add to the histogram. - */ - _telemetryAdd: function(histogramId, value) { - try { - this.mozLoop.telemetryAdd(histogramId, value); - } catch (err) { - console.error("Error recording telemetry", err); - } - }, }; return Client; diff --git a/browser/components/loop/content/js/contacts.js b/browser/components/loop/content/js/contacts.js index ed8281e6489..bdfca3103a8 100644 --- a/browser/components/loop/content/js/contacts.js +++ b/browser/components/loop/content/js/contacts.js @@ -263,6 +263,11 @@ loop.contacts = (function(_, mozL10n) { loop.shared.mixins.WindowCloseMixin ], + propTypes: { + notifications: React.PropTypes.instanceOf( + loop.shared.models.NotificationCollection).isRequired + }, + /** * Contacts collection object */ @@ -389,10 +394,14 @@ loop.contacts = (function(_, mozL10n) { service: "google" }, (err, stats) => { this.setState({ importBusy: false }); - // TODO: bug 1076764 - proper error and success reporting. if (err) { - throw err; + console.error("Contact import error", err); + this.props.notifications.errorL10n("import_contacts_failure_message"); + return; } + this.props.notifications.successL10n("import_contacts_success_message", { + total: stats.total + }); }); }, diff --git a/browser/components/loop/content/js/contacts.jsx b/browser/components/loop/content/js/contacts.jsx index af80a7b48ac..af20771bfec 100644 --- a/browser/components/loop/content/js/contacts.jsx +++ b/browser/components/loop/content/js/contacts.jsx @@ -263,6 +263,11 @@ loop.contacts = (function(_, mozL10n) { loop.shared.mixins.WindowCloseMixin ], + propTypes: { + notifications: React.PropTypes.instanceOf( + loop.shared.models.NotificationCollection).isRequired + }, + /** * Contacts collection object */ @@ -389,10 +394,14 @@ loop.contacts = (function(_, mozL10n) { service: "google" }, (err, stats) => { this.setState({ importBusy: false }); - // TODO: bug 1076764 - proper error and success reporting. if (err) { - throw err; + console.error("Contact import error", err); + this.props.notifications.errorL10n("import_contacts_failure_message"); + return; } + this.props.notifications.successL10n("import_contacts_success_message", { + total: stats.total + }); }); }, diff --git a/browser/components/loop/content/js/conversationViews.js b/browser/components/loop/content/js/conversationViews.js index 90758f51528..69f353fd1d6 100644 --- a/browser/components/loop/content/js/conversationViews.js +++ b/browser/components/loop/content/js/conversationViews.js @@ -835,6 +835,10 @@ loop.conversationViews = (function(mozL10n) { }); var OngoingConversationView = React.createClass({displayName: "OngoingConversationView", + mixins: [ + sharedMixins.MediaSetupMixin + ], + propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, video: React.PropTypes.object, @@ -849,75 +853,18 @@ loop.conversationViews = (function(mozL10n) { }, componentDidMount: function() { - /** - * OT inserts inline styles into the markup. Using a listener for - * resize events helps us trigger a full width/height on the element - * so that they update to the correct dimensions. - * XXX: this should be factored as a mixin. - */ - window.addEventListener('orientationchange', this.updateVideoContainer); - window.addEventListener('resize', this.updateVideoContainer); - // 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 // the sdk wouldn't need to know this, but we can't change that. this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({ - publisherConfig: this._getPublisherConfig(), + publisherConfig: this.getDefaultPublisherConfig({ + publishVideo: this.props.video.enabled + }), getLocalElementFunc: this._getElement.bind(this, ".local"), getRemoteElementFunc: this._getElement.bind(this, ".remote") })); }, - componentWillUnmount: function() { - window.removeEventListener('orientationchange', this.updateVideoContainer); - window.removeEventListener('resize', this.updateVideoContainer); - }, - - /** - * Returns either the required DOMNode - * - * @param {String} className The name of the class to get the element for. - */ - _getElement: function(className) { - return this.getDOMNode().querySelector(className); - }, - - /** - * Returns the required configuration for publishing video on the sdk. - */ - _getPublisherConfig: function() { - // height set to 100%" to fix video layout on Google Chrome - // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445 - return { - insertMode: "append", - width: "100%", - height: "100%", - publishVideo: this.props.video.enabled, - style: { - audioLevelDisplayMode: "off", - bugDisplayMode: "off", - buttonDisplayMode: "off", - nameDisplayMode: "off", - videoDisabledDisplayMode: "off" - } - }; - }, - - /** - * Used to update the video container whenever the orientation or size of the - * display area changes. - */ - updateVideoContainer: function() { - var localStreamParent = this._getElement('.local .OT_publisher'); - var remoteStreamParent = this._getElement('.remote .OT_subscriber'); - if (localStreamParent) { - localStreamParent.style.width = "100%"; - } - if (remoteStreamParent) { - remoteStreamParent.style.height = "100%"; - } - }, - /** * Hangs up the call. */ diff --git a/browser/components/loop/content/js/conversationViews.jsx b/browser/components/loop/content/js/conversationViews.jsx index 063526a6053..aae50118991 100644 --- a/browser/components/loop/content/js/conversationViews.jsx +++ b/browser/components/loop/content/js/conversationViews.jsx @@ -835,6 +835,10 @@ loop.conversationViews = (function(mozL10n) { }); var OngoingConversationView = React.createClass({ + mixins: [ + sharedMixins.MediaSetupMixin + ], + propTypes: { dispatcher: React.PropTypes.instanceOf(loop.Dispatcher).isRequired, video: React.PropTypes.object, @@ -849,75 +853,18 @@ loop.conversationViews = (function(mozL10n) { }, componentDidMount: function() { - /** - * OT inserts inline styles into the markup. Using a listener for - * resize events helps us trigger a full width/height on the element - * so that they update to the correct dimensions. - * XXX: this should be factored as a mixin. - */ - window.addEventListener('orientationchange', this.updateVideoContainer); - window.addEventListener('resize', this.updateVideoContainer); - // 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 // the sdk wouldn't need to know this, but we can't change that. this.props.dispatcher.dispatch(new sharedActions.SetupStreamElements({ - publisherConfig: this._getPublisherConfig(), + publisherConfig: this.getDefaultPublisherConfig({ + publishVideo: this.props.video.enabled + }), getLocalElementFunc: this._getElement.bind(this, ".local"), getRemoteElementFunc: this._getElement.bind(this, ".remote") })); }, - componentWillUnmount: function() { - window.removeEventListener('orientationchange', this.updateVideoContainer); - window.removeEventListener('resize', this.updateVideoContainer); - }, - - /** - * Returns either the required DOMNode - * - * @param {String} className The name of the class to get the element for. - */ - _getElement: function(className) { - return this.getDOMNode().querySelector(className); - }, - - /** - * Returns the required configuration for publishing video on the sdk. - */ - _getPublisherConfig: function() { - // height set to 100%" to fix video layout on Google Chrome - // @see https://bugzilla.mozilla.org/show_bug.cgi?id=1020445 - return { - insertMode: "append", - width: "100%", - height: "100%", - publishVideo: this.props.video.enabled, - style: { - audioLevelDisplayMode: "off", - bugDisplayMode: "off", - buttonDisplayMode: "off", - nameDisplayMode: "off", - videoDisabledDisplayMode: "off" - } - }; - }, - - /** - * Used to update the video container whenever the orientation or size of the - * display area changes. - */ - updateVideoContainer: function() { - var localStreamParent = this._getElement('.local .OT_publisher'); - var remoteStreamParent = this._getElement('.remote .OT_subscriber'); - if (localStreamParent) { - localStreamParent.style.width = "100%"; - } - if (remoteStreamParent) { - remoteStreamParent.style.height = "100%"; - } - }, - /** * Hangs up the call. */ diff --git a/browser/components/loop/content/js/panel.js b/browser/components/loop/content/js/panel.js index ffb2c519b31..3f36b224de6 100644 --- a/browser/components/loop/content/js/panel.js +++ b/browser/components/loop/content/js/panel.js @@ -39,9 +39,7 @@ loop.panel = (function(_, mozL10n) { // When we don't need to rely on the pref, this can move back to // getDefaultProps (bug 1100258). return { - selectedTab: this.props.selectedTab || - (navigator.mozLoop.getLoopPref("rooms.enabled") ? - "rooms" : "call") + selectedTab: this.props.selectedTab || "rooms" }; }, @@ -358,157 +356,6 @@ loop.panel = (function(_, mozL10n) { } }); - /** - * Call url result view. - */ - var CallUrlResult = React.createClass({displayName: "CallUrlResult", - mixins: [sharedMixins.DocumentVisibilityMixin], - - propTypes: { - callUrl: React.PropTypes.string, - callUrlExpiry: React.PropTypes.number, - notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired - }, - - getInitialState: function() { - return { - pending: false, - copied: false, - callUrl: this.props.callUrl || "", - callUrlExpiry: 0 - }; - }, - - /** - * Provided by DocumentVisibilityMixin. Schedules retrieval of a new call - * URL everytime the panel is reopened. - */ - onDocumentVisible: function() { - this._fetchCallUrl(); - }, - - componentDidMount: function() { - // If we've already got a callURL, don't bother requesting a new one. - // As of this writing, only used for visual testing in the UI showcase. - if (this.state.callUrl.length) { - return; - } - - this._fetchCallUrl(); - }, - - /** - * Fetches a call URL. - */ - _fetchCallUrl: function() { - this.setState({pending: true}); - // XXX This is an empty string as a conversation identifier. Bug 1015938 implements - // a user-set string. - this.props.client.requestCallUrl("", - this._onCallUrlReceived); - }, - - _onCallUrlReceived: function(err, callUrlData) { - if (err) { - if (err.code != 401) { - // 401 errors are already handled in hawkRequest and show an error - // message about the session. - this.props.notifications.errorL10n("unable_retrieve_url"); - } - this.setState(this.getInitialState()); - } else { - try { - var callUrl = new window.URL(callUrlData.callUrl); - // XXX the current server vers does not implement the callToken field - // but it exists in the API. This workaround should be removed in the future - var token = callUrlData.callToken || - callUrl.pathname.split('/').pop(); - - // Now that a new URL is available, indicate it has not been shared. - this.linkExfiltrated = false; - - this.setState({pending: false, copied: false, - callUrl: callUrl.href, - callUrlExpiry: callUrlData.expiresAt}); - } catch(e) { - console.log(e); - this.props.notifications.errorL10n("unable_retrieve_url"); - this.setState(this.getInitialState()); - } - } - }, - - handleEmailButtonClick: function(event) { - this.handleLinkExfiltration(event); - - sharedUtils.composeCallUrlEmail(this.state.callUrl); - }, - - handleCopyButtonClick: function(event) { - this.handleLinkExfiltration(event); - // XXX the mozLoop object should be passed as a prop, to ease testing and - // using a fake implementation in UI components showcase. - navigator.mozLoop.copyString(this.state.callUrl); - this.setState({copied: true}); - }, - - linkExfiltrated: false, - - handleLinkExfiltration: function(event) { - // Update the count of shared URLs only once per generated URL. - if (!this.linkExfiltrated) { - this.linkExfiltrated = true; - try { - navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true); - } catch (err) { - console.error("Error recording telemetry", err); - } - } - - // Note URL expiration every time it is shared. - if (this.state.callUrlExpiry) { - navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry); - } - }, - - render: function() { - // XXX setting elem value from a state (in the callUrl input) - // makes it immutable ie read only but that is fine in our case. - // readOnly attr will suppress a warning regarding this issue - // from the react lib. - var cx = React.addons.classSet; - return ( - React.createElement("div", {className: "generate-url"}, - React.createElement("header", {id: "share-link-header"}, mozL10n.get("share_link_header_text")), - React.createElement("div", {className: "generate-url-stack"}, - React.createElement("input", {type: "url", value: this.state.callUrl, readOnly: "true", - onCopy: this.handleLinkExfiltration, - className: cx({"generate-url-input": true, - pending: this.state.pending, - // Used in functional testing, signals that - // call url was received from loop server - callUrl: !this.state.pending})}), - React.createElement("div", {className: cx({"generate-url-spinner": true, - spinner: true, - busy: this.state.pending})}) - ), - React.createElement(ButtonGroup, {additionalClass: "url-actions"}, - React.createElement(Button, {additionalClass: "button-email", - disabled: !this.state.callUrl, - onClick: this.handleEmailButtonClick, - caption: mozL10n.get("share_button")}), - React.createElement(Button, {additionalClass: "button-copy", - disabled: !this.state.callUrl, - onClick: this.handleCopyButtonClick, - caption: this.state.copied ? mozL10n.get("copied_url_button") : - mozL10n.get("copy_url_button")}) - ) - ) - ); - } - }); - /** * FxA sign in/up link component. */ @@ -820,9 +667,7 @@ loop.panel = (function(_, mozL10n) { var PanelView = React.createClass({displayName: "PanelView", propTypes: { notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired, // Mostly used for UI components showcase and unit tests - callUrl: React.PropTypes.string, userProfile: React.PropTypes.object, // Used only for unit tests. showTabButtons: React.PropTypes.bool, @@ -869,17 +714,13 @@ loop.panel = (function(_, mozL10n) { } }, - _roomsEnabled: function() { - return this.props.mozLoop.getLoopPref("rooms.enabled"); - }, - _onStatusChanged: function() { var profile = this.props.mozLoop.userProfile; var currUid = this.state.userProfile ? this.state.userProfile.uid : null; var newUid = profile ? profile.uid : null; if (currUid != newUid) { // On profile change (login, logout), switch back to the default tab. - this.selectTab(this._roomsEnabled() ? "rooms" : "call"); + this.selectTab("rooms"); this.setState({userProfile: profile}); } this.updateServiceErrors(); @@ -902,34 +743,6 @@ loop.panel = (function(_, mozL10n) { } }, - /** - * The rooms feature is hidden by default for now. Once it gets mainstream, - * this method can be simplified. - */ - _renderRoomsOrCallTab: function() { - if (!this._roomsEnabled()) { - return ( - React.createElement(Tab, {name: "call"}, - React.createElement("div", {className: "content-area"}, - React.createElement(CallUrlResult, {client: this.props.client, - notifications: this.props.notifications, - callUrl: this.props.callUrl}), - React.createElement(ToSView, null) - ) - ) - ); - } - - return ( - React.createElement(Tab, {name: "rooms"}, - React.createElement(RoomList, {dispatcher: this.props.dispatcher, - store: this.props.roomStore, - userDisplayName: this._getUserDisplayName()}), - React.createElement(ToSView, null) - ) - ); - }, - startForm: function(name, contact) { this.refs[name].initForm(contact); this.selectTab(name); @@ -986,10 +799,16 @@ loop.panel = (function(_, mozL10n) { clearOnDocumentHidden: true}), React.createElement(TabView, {ref: "tabView", selectedTab: this.props.selectedTab, buttonsHidden: hideButtons}, - this._renderRoomsOrCallTab(), + React.createElement(Tab, {name: "rooms"}, + React.createElement(RoomList, {dispatcher: this.props.dispatcher, + store: this.props.roomStore, + userDisplayName: this._getUserDisplayName()}), + React.createElement(ToSView, null) + ), React.createElement(Tab, {name: "contacts"}, React.createElement(ContactsList, {selectTab: this.selectTab, - startForm: this.startForm}) + startForm: this.startForm, + notifications: this.props.notifications}) ), React.createElement(Tab, {name: "contacts_add", hidden: true}, React.createElement(ContactDetailsForm, {ref: "contacts_add", mode: "add", @@ -1028,7 +847,6 @@ loop.panel = (function(_, mozL10n) { // else to ensure the L10n environment is setup correctly. mozL10n.initialize(navigator.mozLoop); - var client = new loop.Client(); var notifications = new sharedModels.NotificationCollection(); var dispatcher = new loop.Dispatcher(); var roomStore = new loop.store.RoomStore(dispatcher, { @@ -1037,7 +855,6 @@ loop.panel = (function(_, mozL10n) { }); React.render(React.createElement(PanelView, { - client: client, notifications: notifications, roomStore: roomStore, mozLoop: navigator.mozLoop, @@ -1056,7 +873,6 @@ loop.panel = (function(_, mozL10n) { init: init, AuthLink: AuthLink, AvailabilityDropdown: AvailabilityDropdown, - CallUrlResult: CallUrlResult, GettingStartedView: GettingStartedView, PanelView: PanelView, RoomEntry: RoomEntry, diff --git a/browser/components/loop/content/js/panel.jsx b/browser/components/loop/content/js/panel.jsx index 20afd56c0c6..736dcfc7702 100644 --- a/browser/components/loop/content/js/panel.jsx +++ b/browser/components/loop/content/js/panel.jsx @@ -39,9 +39,7 @@ loop.panel = (function(_, mozL10n) { // When we don't need to rely on the pref, this can move back to // getDefaultProps (bug 1100258). return { - selectedTab: this.props.selectedTab || - (navigator.mozLoop.getLoopPref("rooms.enabled") ? - "rooms" : "call") + selectedTab: this.props.selectedTab || "rooms" }; }, @@ -358,157 +356,6 @@ loop.panel = (function(_, mozL10n) { } }); - /** - * Call url result view. - */ - var CallUrlResult = React.createClass({ - mixins: [sharedMixins.DocumentVisibilityMixin], - - propTypes: { - callUrl: React.PropTypes.string, - callUrlExpiry: React.PropTypes.number, - notifications: React.PropTypes.object.isRequired, - client: React.PropTypes.object.isRequired - }, - - getInitialState: function() { - return { - pending: false, - copied: false, - callUrl: this.props.callUrl || "", - callUrlExpiry: 0 - }; - }, - - /** - * Provided by DocumentVisibilityMixin. Schedules retrieval of a new call - * URL everytime the panel is reopened. - */ - onDocumentVisible: function() { - this._fetchCallUrl(); - }, - - componentDidMount: function() { - // If we've already got a callURL, don't bother requesting a new one. - // As of this writing, only used for visual testing in the UI showcase. - if (this.state.callUrl.length) { - return; - } - - this._fetchCallUrl(); - }, - - /** - * Fetches a call URL. - */ - _fetchCallUrl: function() { - this.setState({pending: true}); - // XXX This is an empty string as a conversation identifier. Bug 1015938 implements - // a user-set string. - this.props.client.requestCallUrl("", - this._onCallUrlReceived); - }, - - _onCallUrlReceived: function(err, callUrlData) { - if (err) { - if (err.code != 401) { - // 401 errors are already handled in hawkRequest and show an error - // message about the session. - this.props.notifications.errorL10n("unable_retrieve_url"); - } - this.setState(this.getInitialState()); - } else { - try { - var callUrl = new window.URL(callUrlData.callUrl); - // XXX the current server vers does not implement the callToken field - // but it exists in the API. This workaround should be removed in the future - var token = callUrlData.callToken || - callUrl.pathname.split('/').pop(); - - // Now that a new URL is available, indicate it has not been shared. - this.linkExfiltrated = false; - - this.setState({pending: false, copied: false, - callUrl: callUrl.href, - callUrlExpiry: callUrlData.expiresAt}); - } catch(e) { - console.log(e); - this.props.notifications.errorL10n("unable_retrieve_url"); - this.setState(this.getInitialState()); - } - } - }, - - handleEmailButtonClick: function(event) { - this.handleLinkExfiltration(event); - - sharedUtils.composeCallUrlEmail(this.state.callUrl); - }, - - handleCopyButtonClick: function(event) { - this.handleLinkExfiltration(event); - // XXX the mozLoop object should be passed as a prop, to ease testing and - // using a fake implementation in UI components showcase. - navigator.mozLoop.copyString(this.state.callUrl); - this.setState({copied: true}); - }, - - linkExfiltrated: false, - - handleLinkExfiltration: function(event) { - // Update the count of shared URLs only once per generated URL. - if (!this.linkExfiltrated) { - this.linkExfiltrated = true; - try { - navigator.mozLoop.telemetryAdd("LOOP_CLIENT_CALL_URL_SHARED", true); - } catch (err) { - console.error("Error recording telemetry", err); - } - } - - // Note URL expiration every time it is shared. - if (this.state.callUrlExpiry) { - navigator.mozLoop.noteCallUrlExpiry(this.state.callUrlExpiry); - } - }, - - render: function() { - // XXX setting elem value from a state (in the callUrl input) - // makes it immutable ie read only but that is fine in our case. - // readOnly attr will suppress a warning regarding this issue - // from the react lib. - var cx = React.addons.classSet; - return ( -