diff options
Diffstat (limited to 'browser/components/newtab/lib/BookmarkPanelHub.jsm')
-rw-r--r-- | browser/components/newtab/lib/BookmarkPanelHub.jsm | 332 |
1 files changed, 332 insertions, 0 deletions
diff --git a/browser/components/newtab/lib/BookmarkPanelHub.jsm b/browser/components/newtab/lib/BookmarkPanelHub.jsm new file mode 100644 index 0000000000..e426ea7ed6 --- /dev/null +++ b/browser/components/newtab/lib/BookmarkPanelHub.jsm @@ -0,0 +1,332 @@ +/* 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"]; |