diff options
Diffstat (limited to 'browser/components/privatebrowsing/content/aboutPrivateBrowsing.js')
-rw-r--r-- | browser/components/privatebrowsing/content/aboutPrivateBrowsing.js | 425 |
1 files changed, 425 insertions, 0 deletions
diff --git a/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js new file mode 100644 index 0000000000..2a64c064a5 --- /dev/null +++ b/browser/components/privatebrowsing/content/aboutPrivateBrowsing.js @@ -0,0 +1,425 @@ +/* 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/. */ + +/* eslint-env mozilla/remote-page */ + +/** + * Determines whether a given value is a fluent id or plain text and adds it to an element + * @param {Array<[HTMLElement, string]>} items An array of [element, value] where value is + * a fluent id starting with "fluent:" or plain text + */ +function translateElements(items) { + items.forEach(([element, value]) => { + // Skip empty text or elements + if (!element || !value) { + return; + } + const fluentId = value.replace(/^fluent:/, ""); + if (fluentId !== value) { + document.l10n.setAttributes(element, fluentId); + } else { + element.textContent = value; + element.removeAttribute("data-l10n-id"); + } + }); +} + +function renderInfo({ + infoEnabled = false, + infoTitle, + infoTitleEnabled, + infoBody, + infoLinkText, + infoLinkUrl, + infoIcon, +} = {}) { + const container = document.querySelector(".info"); + if (!infoEnabled) { + container.remove(); + return; + } + + const titleEl = document.getElementById("info-title"); + const bodyEl = document.getElementById("info-body"); + const linkEl = document.getElementById("private-browsing-myths"); + + if (infoIcon) { + container.style.backgroundImage = `url(${infoIcon})`; + } + + if (!infoTitleEnabled) { + titleEl.remove(); + } + + translateElements([ + [titleEl, infoTitle], + [bodyEl, infoBody], + [linkEl, infoLinkText], + ]); + + linkEl.setAttribute( + "href", + infoLinkUrl || + RPMGetFormatURLPref("app.support.baseURL") + "private-browsing-myths" + ); + linkEl.setAttribute("target", "_blank"); + + linkEl.addEventListener("click", () => { + window.PrivateBrowsingRecordClick("info_link"); + }); +} + +async function renderPromo({ + messageId = null, + promoEnabled = false, + promoType = "VPN", + promoTitle, + promoTitleEnabled, + promoLinkText, + promoLinkType, + promoSectionStyle, + promoHeader, + promoImageLarge, + promoImageSmall, + promoButton = null, +} = {}) { + const shouldShow = await RPMSendQuery("ShouldShowPromo", { type: promoType }); + const container = document.querySelector(".promo"); + + if (!promoEnabled || !shouldShow) { + container.remove(); + return false; + } + + const titleEl = document.getElementById("private-browsing-promo-text"); + const linkEl = document.getElementById("private-browsing-promo-link"); + const promoHeaderEl = document.getElementById("promo-header"); + const infoContainerEl = document.querySelector(".info"); + const promoImageLargeEl = document.querySelector(".promo-image-large img"); + const promoImageSmallEl = document.querySelector(".promo-image-small img"); + const dismissBtn = document.querySelector("#dismiss-btn"); + + if (promoLinkType === "link") { + linkEl.classList.remove("primary"); + linkEl.classList.add("text-link", "promo-link"); + } + + if (promoButton?.action) { + linkEl.addEventListener("click", async event => { + event.preventDefault(); + + // Record promo click telemetry and set metrics as allow for spotlight + // modal opened on promo click if user is enrolled in an experiment + let isExperiment = window.PrivateBrowsingRecordClick("promo_link"); + const promoButtonData = promoButton?.action?.data; + if ( + promoButton?.action?.type === "SHOW_SPOTLIGHT" && + promoButtonData?.content + ) { + promoButtonData.content.metrics = isExperiment ? "allow" : "block"; + } + + await RPMSendQuery("SpecialMessageActionDispatch", promoButton.action); + }); + } else { + // If the action doesn't exist, remove the promo completely + container.remove(); + return false; + } + + const onDismissBtnClick = () => { + window.ASRouterMessage({ + type: "BLOCK_MESSAGE_BY_ID", + data: { id: messageId }, + }); + window.PrivateBrowsingRecordClick("dismiss_button"); + container.remove(); + }; + + if (dismissBtn && messageId) { + dismissBtn.addEventListener("click", onDismissBtnClick, { once: true }); + } + + if (promoSectionStyle) { + container.classList.add(promoSectionStyle); + + switch (promoSectionStyle) { + case "below-search": + container.remove(); + infoContainerEl?.insertAdjacentElement("beforebegin", container); + break; + case "top": + container.remove(); + document.body.insertAdjacentElement("afterbegin", container); + } + } + + if (promoImageLarge) { + promoImageLargeEl.src = promoImageLarge; + } else { + promoImageLargeEl.parentNode.remove(); + } + + if (promoImageSmall) { + promoImageSmallEl.src = promoImageSmall; + } else { + promoImageSmallEl.parentNode.remove(); + } + + if (!promoTitleEnabled) { + titleEl.remove(); + } + + if (!promoHeader) { + promoHeaderEl.remove(); + } + + translateElements([ + [titleEl, promoTitle], + [linkEl, promoLinkText], + [promoHeaderEl, promoHeader], + ]); + + // Only make promo section visible after adding content + // and translations to prevent layout shifting in page + container.classList.add("promo-visible"); + return true; +} + +/** + * For every PB newtab loaded, a second is pre-rendered in the background. + * We need to guard against invalid impressions by checking visibility state. + * If visible, record. Otherwise, listen for visibility change and record later. + */ +function recordOnceVisible(message) { + const recordImpression = () => { + if (document.visibilityState === "visible") { + window.ASRouterMessage({ + type: "IMPRESSION", + data: message, + }); + // Similar telemetry, but for Nimbus experiments + window.PrivateBrowsingExposureTelemetry(); + document.removeEventListener("visibilitychange", recordImpression); + } + }; + + if (document.visibilityState === "visible") { + window.ASRouterMessage({ + type: "IMPRESSION", + data: message, + }); + // Similar telemetry, but for Nimbus experiments + window.PrivateBrowsingExposureTelemetry(); + } else { + document.addEventListener("visibilitychange", recordImpression); + } +} + +// The PB newtab may be pre-rendered. Once the tab is visible, check to make sure the message wasn't blocked after the initial render. If it was, remove the promo. +function handlePromoOnPreload(message) { + async function removePromoIfBlocked() { + if (document.visibilityState === "visible") { + let blocked = await RPMSendQuery("IsPromoBlocked", message); + if (blocked) { + const container = document.querySelector(".promo"); + container.remove(); + } + } + document.removeEventListener("visibilitychange", removePromoIfBlocked); + } + // Only add the listener to pre-rendered tabs that aren't visible + if (document.visibilityState !== "visible") { + document.addEventListener("visibilitychange", removePromoIfBlocked); + } +} + +async function setupMessageConfig(config = null) { + let message = null; + + if (!config) { + let hideDefault = window.PrivateBrowsingShouldHideDefault(); + try { + let response = await window.ASRouterMessage({ + type: "PBNEWTAB_MESSAGE_REQUEST", + data: { hideDefault: !!hideDefault }, + }); + message = response?.message; + config = message?.content; + config.messageId = message?.id; + } catch (e) {} + } + + renderInfo(config); + let hasRendered = await renderPromo(config); + if (hasRendered && message) { + recordOnceVisible(message); + handlePromoOnPreload(message); + } + // For tests + document.documentElement.setAttribute("PrivateBrowsingRenderComplete", true); +} + +let SHOW_DEVTOOLS_MESSAGE = "ShowDevToolsMessage"; + +function showDevToolsMessage(msg) { + msg.data.content.messageId = "DEVTOOLS_MESSAGE"; + setupMessageConfig(msg?.data?.content); + RPMRemoveMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage); +} + +document.addEventListener("DOMContentLoaded", function () { + // check the url to see if we're rendering a devtools message + if (document.location.toString().includes("debug")) { + RPMAddMessageListener(SHOW_DEVTOOLS_MESSAGE, showDevToolsMessage); + return; + } + if (!RPMIsWindowPrivate()) { + document.documentElement.classList.remove("private"); + document.documentElement.classList.add("normal"); + document + .getElementById("startPrivateBrowsing") + .addEventListener("click", function () { + RPMSendAsyncMessage("OpenPrivateWindow"); + }); + return; + } + + let newLogoEnabled = window.PrivateBrowsingEnableNewLogo(); + document + .getElementById("about-private-browsing-logo") + .toggleAttribute("legacy", !newLogoEnabled); + + // We don't do this setup until now, because we don't want to record any impressions until we're + // sure we're actually running a private window, not just about:privatebrowsing in a normal window. + setupMessageConfig(); + + // Set up the private search banner. + const privateSearchBanner = document.getElementById("search-banner"); + + RPMSendQuery("ShouldShowSearchBanner", {}).then(engineName => { + if (engineName) { + document.l10n.setAttributes( + document.getElementById("about-private-browsing-search-banner-title"), + "about-private-browsing-search-banner-title", + { engineName } + ); + privateSearchBanner.removeAttribute("hidden"); + document.body.classList.add("showBanner"); + } + + // We set this attribute so that tests know when we are done. + document.documentElement.setAttribute("SearchBannerInitialized", true); + }); + + function hideSearchBanner() { + privateSearchBanner.hidden = true; + document.body.classList.remove("showBanner"); + RPMSendAsyncMessage("SearchBannerDismissed"); + } + + document + .getElementById("search-banner-close-button") + .addEventListener("click", () => { + hideSearchBanner(); + }); + + let openSearchOptions = document.getElementById( + "about-private-browsing-search-banner-description" + ); + let openSearchOptionsEvtHandler = evt => { + if ( + evt.target.id == "open-search-options-link" && + (evt.keyCode == evt.DOM_VK_RETURN || evt.type == "click") + ) { + RPMSendAsyncMessage("OpenSearchPreferences"); + hideSearchBanner(); + } + }; + openSearchOptions.addEventListener("click", openSearchOptionsEvtHandler); + openSearchOptions.addEventListener("keypress", openSearchOptionsEvtHandler); + + // Setup the search hand-off box. + let btn = document.getElementById("search-handoff-button"); + RPMSendQuery("ShouldShowSearch", {}).then( + ([engineName, shouldHandOffToSearchMode]) => { + let input = document.querySelector(".fake-textbox"); + if (shouldHandOffToSearchMode) { + document.l10n.setAttributes(btn, "about-private-browsing-search-btn"); + document.l10n.setAttributes( + input, + "about-private-browsing-search-placeholder" + ); + } else if (engineName) { + document.l10n.setAttributes(btn, "about-private-browsing-handoff", { + engine: engineName, + }); + document.l10n.setAttributes( + input, + "about-private-browsing-handoff-text", + { + engine: engineName, + } + ); + } else { + document.l10n.setAttributes( + btn, + "about-private-browsing-handoff-no-engine" + ); + document.l10n.setAttributes( + input, + "about-private-browsing-handoff-text-no-engine" + ); + } + } + ); + + let editable = document.getElementById("fake-editable"); + let DISABLE_SEARCH_TOPIC = "DisableSearch"; + let SHOW_SEARCH_TOPIC = "ShowSearch"; + let SEARCH_HANDOFF_TOPIC = "SearchHandoff"; + + function showSearch() { + btn.classList.remove("focused"); + btn.classList.remove("disabled"); + RPMRemoveMessageListener(SHOW_SEARCH_TOPIC, showSearch); + } + + function disableSearch() { + btn.classList.add("disabled"); + } + + function handoffSearch(text) { + RPMSendAsyncMessage(SEARCH_HANDOFF_TOPIC, { text }); + RPMAddMessageListener(SHOW_SEARCH_TOPIC, showSearch); + if (text) { + disableSearch(); + } else { + btn.classList.add("focused"); + RPMAddMessageListener(DISABLE_SEARCH_TOPIC, disableSearch); + } + } + btn.addEventListener("focus", function () { + handoffSearch(); + }); + btn.addEventListener("click", function () { + handoffSearch(); + }); + + // Hand-off any text that gets dropped or pasted + editable.addEventListener("drop", function (ev) { + ev.preventDefault(); + let text = ev.dataTransfer.getData("text"); + if (text) { + handoffSearch(text); + } + }); + editable.addEventListener("paste", function (ev) { + ev.preventDefault(); + handoffSearch(ev.clipboardData.getData("Text")); + }); + + // Load contentSearchUI so it sets the search engine icon and name for us. + new window.ContentSearchHandoffUIController(); +}); |