fune/browser/components/newtab/lib/BookmarkPanelHub.jsm
Bernard Igiri 24f9294ade Bug 1614465 - Replacing ASRouter calls to MessageChannel with JSWindowActors to eliminate ASRouterFeed r=k88hudson
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
2020-10-21 20:04:13 +00:00

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"];