forked from mirrors/gecko-dev
		
	Patch by Bernard Igiri <bigiri@mozilla.com> Replacing async events with async method calls that use JSWindowActors to communicate with the parent process. This will simplify these calls, bring the relevant code into local scope, and eliminate the need for MessageChannel. Eliminating the MessageChannel dependency allows us to move the ASRouter initialization out of ASRouterFeed and into JSWindowActors. Differential Revision: https://phabricator.services.mozilla.com/D71796
		
			
				
	
	
		
			332 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
			
		
		
	
	
			332 lines
		
	
	
	
		
			10 KiB
		
	
	
	
		
			JavaScript
		
	
	
	
	
	
/* 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"];
 |