/* 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"; ChromeUtils.defineModuleGetter( this, "FxAccounts", "resource://gre/modules/FxAccounts.jsm" ); ChromeUtils.defineModuleGetter( this, "Services", "resource://gre/modules/Services.jsm" ); ChromeUtils.defineModuleGetter( this, "PrivateBrowsingUtils", "resource://gre/modules/PrivateBrowsingUtils.jsm" ); class _BookmarkPanelHub { constructor() { this._id = "BookmarkPanelHub"; this._trigger = { id: "bookmark-panel" }; this._handleMessageRequest = null; this._addImpression = null; this._sendTelemetry = null; this._initialized = false; this._response = null; this._l10n = null; this.messageRequest = this.messageRequest.bind(this); this.toggleRecommendation = this.toggleRecommendation.bind(this); this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this); this.collapseMessage = this.collapseMessage.bind(this); } /** * @param {function} handleMessageRequest * @param {function} addImpression * @param {function} sendTelemetry - Used for sending user telemetry information */ init(handleMessageRequest, addImpression, sendTelemetry) { this._handleMessageRequest = handleMessageRequest; this._addImpression = addImpression; this._sendTelemetry = sendTelemetry; this._l10n = new DOMLocalization([]); this._initialized = true; } uninit() { this._l10n = null; this._initialized = false; this._handleMessageRequest = null; this._addImpression = null; this._sendTelemetry = null; this._response = null; } /** * Checks if a similar cached requests exists before forwarding the request * to ASRouter. Caches only 1 request, unique identifier is `request.url`. * Caching ensures we don't duplicate requests and telemetry pings. * Return value is important for the caller to know if a message will be * shown. * * @returns {obj|null} response object or null if no messages matched */ async messageRequest(target, win) { if (!this._initialized) { return false; } if ( this._response && this._response.win === win && this._response.url === target.url && this._response.content ) { this.showMessage(this._response.content, target, win); return true; } // If we didn't match on a previously cached request then make sure // the container is empty this._removeContainer(target); const response = await this._handleMessageRequest({ triggerId: this._trigger.id, }); return this.onResponse(response, target, win); } /** * If the response contains a message render it and send an impression. * Otherwise we remove the message from the container. */ onResponse(response, target, win) { this._response = { ...response, collapsed: false, target, win, url: target.url, }; if (response && response.content) { // Only insert localization files if we need to show a message win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); win.MozXULElement.insertFTLIfNeeded("browser/branding/sync-brand.ftl"); this.showMessage(response.content, target, win); this.sendImpression(); this.sendUserEventTelemetry("IMPRESSION", win); } else { this.hideMessage(target); } target.infoButton.disabled = !response; return !!response; } showMessage(message, target, win) { if (this._response && this._response.collapsed) { this.toggleRecommendation(false); return; } const createElement = elem => target.document.createElementNS("http://www.w3.org/1999/xhtml", elem); let recommendation = target.container.querySelector("#cfrMessageContainer"); if (!recommendation) { recommendation = createElement("div"); const headerContainer = createElement("div"); headerContainer.classList.add("cfrMessageHeader"); recommendation.setAttribute("id", "cfrMessageContainer"); recommendation.addEventListener("click", async e => { target.hidePopup(); const url = await FxAccounts.config.promiseConnectAccountURI( "bookmark" ); win.ownerGlobal.openLinkIn(url, "tabshifted", { private: false, triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal( {} ), csp: null, }); this.sendUserEventTelemetry("CLICK", win); }); recommendation.style.color = message.color; recommendation.style.background = `linear-gradient(135deg, ${message.background_color_1} 0%, ${message.background_color_2} 70%)`; const close = createElement("button"); close.setAttribute("id", "cfrClose"); close.setAttribute("aria-label", "close"); close.addEventListener("click", e => { this.sendUserEventTelemetry("DISMISS", win); this.collapseMessage(); target.close(e); }); const title = createElement("h1"); title.setAttribute("id", "editBookmarkPanelRecommendationTitle"); const content = createElement("p"); content.setAttribute("id", "editBookmarkPanelRecommendationContent"); const cta = createElement("button"); cta.setAttribute("id", "editBookmarkPanelRecommendationCta"); // If `string_id` is present it means we are relying on fluent for translations if (message.text.string_id) { this._l10n.setAttributes( close, message.close_button.tooltiptext.string_id ); this._l10n.setAttributes(title, message.title.string_id); this._l10n.setAttributes(content, message.text.string_id); this._l10n.setAttributes(cta, message.cta.string_id); } else { close.setAttribute("title", message.close_button.tooltiptext); title.textContent = message.title; content.textContent = message.text; cta.textContent = message.cta; } headerContainer.appendChild(title); headerContainer.appendChild(close); recommendation.appendChild(headerContainer); recommendation.appendChild(content); recommendation.appendChild(cta); target.container.appendChild(recommendation); } this.toggleRecommendation(true); this._adjustPanelHeight(win, recommendation); } /** * Adjust the size of the container for locales where the message is * longer than the fixed 150px set for height */ async _adjustPanelHeight(window, messageContainer) { const { document } = window; // Contains the screenshot of the page we are bookmarking const screenshotContainer = document.getElementById( "editBookmarkPanelImage" ); // Wait for strings to be added which can change element height await document.l10n.translateElements([messageContainer]); window.requestAnimationFrame(() => { let { height } = messageContainer.getBoundingClientRect(); if (height > 150) { messageContainer.classList.add("longMessagePadding"); // Get the new value with the added padding height = messageContainer.getBoundingClientRect().height; // Needs to be adjusted to match the message height screenshotContainer.style.height = `${height}px`; } }); } /** * Restore the panel back to the original size so the slide in * animation can run again */ _restorePanelHeight(window) { const { document } = window; // Contains the screenshot of the page we are bookmarking document.getElementById("editBookmarkPanelImage").style.height = ""; } toggleRecommendation(visible) { if (!this._response) { return; } const { target } = this._response; if (visible === undefined) { // When called from the info button of the bookmark panel target.infoButton.checked = !target.infoButton.checked; } else { target.infoButton.checked = visible; } if (target.infoButton.checked) { // If it was ever collapsed we need to cancel the state this._response.collapsed = false; target.container.removeAttribute("disabled"); } else { target.container.setAttribute("disabled", "disabled"); } } collapseMessage() { this._response.collapsed = true; this.toggleRecommendation(false); } _removeContainer(target) { if (target || (this._response && this._response.target)) { const container = ( target || this._response.target ).container.querySelector("#cfrMessageContainer"); if (container) { this._restorePanelHeight(this._response.win); container.remove(); } } } hideMessage(target) { this._removeContainer(target); this.toggleRecommendation(false); this._response = null; } forceShowMessage(browser, message) { const doc = browser.ownerGlobal.gBrowser.ownerDocument; const win = browser.ownerGlobal.window; const panelTarget = { container: doc.getElementById("editBookmarkPanelRecommendation"), infoButton: doc.getElementById("editBookmarkPanelInfoButton"), document: doc, close: e => { e.stopPropagation(); this.toggleRecommendation(false); }, }; // Remove any existing message this.hideMessage(panelTarget); // Reset the reference to the panel elements this._response = { target: panelTarget, win }; // Required if we want to preview messages that include fluent strings win.MozXULElement.insertFTLIfNeeded("browser/newtab/asrouter.ftl"); win.MozXULElement.insertFTLIfNeeded("browser/branding/sync-brand.ftl"); this.showMessage(message.content, panelTarget, win); } sendImpression() { this._addImpression(this._response); } sendUserEventTelemetry(event, win) { // Only send pings for non private browsing windows if ( !PrivateBrowsingUtils.isBrowserPrivate( win.ownerGlobal.gBrowser.selectedBrowser ) ) { this._sendPing({ message_id: this._response.id, bucket_id: this._response.id, event, }); } } _sendPing(ping) { this._sendTelemetry({ type: "DOORHANGER_TELEMETRY", data: { action: "cfr_user_event", source: "CFR", ...ping }, }); } } this._BookmarkPanelHub = _BookmarkPanelHub; /** * BookmarkPanelHub - singleton instance of _BookmarkPanelHub that can initiate * message requests and render messages. */ this.BookmarkPanelHub = new _BookmarkPanelHub(); const EXPORTED_SYMBOLS = ["BookmarkPanelHub", "_BookmarkPanelHub"];