761 lines
26 KiB
JavaScript
761 lines
26 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/. */
|
|
|
|
/* eslint-env mozilla/remote-page */
|
|
|
|
import { MozLitElement } from "chrome://global/content/lit-utils.mjs";
|
|
import { html, ifDefined } from "chrome://global/content/vendor/lit.all.mjs";
|
|
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/highlights.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/settings.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/adjusted-rating.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/reliability.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/analysis-explainer.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/shopping-message-bar.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/unanalyzed.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/recommended-ad.mjs";
|
|
// eslint-disable-next-line import/no-unassigned-import
|
|
import "chrome://browser/content/shopping/new-position-notification-card.mjs";
|
|
|
|
// The number of pixels that must be scrolled from the
|
|
// top of the sidebar to show the header box shadow.
|
|
const HEADER_SCROLL_PIXEL_OFFSET = 8;
|
|
|
|
const HEADER_NOT_TEXT_WRAPPED_HEIGHT = 32;
|
|
const SIDEBAR_CLOSED_COUNT_PREF =
|
|
"browser.shopping.experience2023.sidebarClosedCount";
|
|
const SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF =
|
|
"browser.shopping.experience2023.showKeepSidebarClosedMessage";
|
|
const SHOPPING_SIDEBAR_ACTIVE_PREF = "browser.shopping.experience2023.active";
|
|
const SIDEBAR_REVAMP_PREF = "sidebar.revamp";
|
|
const INTEGRATED_SIDEBAR_PREF =
|
|
"browser.shopping.experience2023.integratedSidebar";
|
|
const HAS_SEEN_POSITION_NOTIFICATION_CARD_PREF =
|
|
"browser.shopping.experience2023.newPositionCard.hasSeen";
|
|
|
|
const CLOSED_COUNT_PREVIOUS_MIN = 4;
|
|
const CLOSED_COUNT_PREVIOUS_MAX = 6;
|
|
|
|
export class ShoppingContainer extends MozLitElement {
|
|
static properties = {
|
|
data: { type: Object },
|
|
showOnboarding: { type: Boolean },
|
|
productUrl: { type: String },
|
|
recommendationData: { type: Array },
|
|
isOffline: { type: Boolean },
|
|
analysisEvent: { type: Object },
|
|
userReportedAvailable: { type: Boolean },
|
|
adsEnabled: { type: Boolean },
|
|
adsEnabledByUser: { type: Boolean },
|
|
isAnalysisInProgress: { type: Boolean },
|
|
analysisProgress: { type: Number },
|
|
showHeaderShadow: { type: Boolean, state: true },
|
|
isOverflow: { type: Boolean },
|
|
autoOpenEnabled: { type: Boolean },
|
|
autoOpenEnabledByUser: { type: Boolean },
|
|
showingKeepClosedMessage: { type: Boolean },
|
|
isProductPage: { type: Boolean },
|
|
isSupportedSite: { type: Boolean },
|
|
supportedDomains: { type: Object },
|
|
formattedDomainList: { type: Object, state: true },
|
|
isHeaderOverflow: { type: Boolean, state: true },
|
|
isSidebarStartPosition: { type: Boolean },
|
|
showNewPositionCard: { type: Boolean },
|
|
};
|
|
|
|
static get queries() {
|
|
return {
|
|
reviewReliabilityEl: "review-reliability",
|
|
adjustedRatingEl: "adjusted-rating",
|
|
highlightsEl: "review-highlights",
|
|
settingsEl: "shopping-settings",
|
|
analysisExplainerEl: "analysis-explainer",
|
|
unanalyzedProductEl: "unanalyzed-product-card",
|
|
shoppingMessageBarEl: "shopping-message-bar",
|
|
recommendedAdEl: "recommended-ad",
|
|
loadingEl: "#loading-wrapper",
|
|
closeButtonEl: "#close-button",
|
|
keepClosedMessageBarEl: "#keep-closed-message-bar",
|
|
emptyStateImgEl: "#shopping-empty-state-img",
|
|
emptyStateHeaderEl: "#shopping-empty-state-header",
|
|
emptyStateTextEl: "#shopping-empty-state-text",
|
|
emptyStateSupportedListEl: "#shopping-empty-list-of-supported-domains",
|
|
containerContentEl: "#content",
|
|
header: "#shopping-header",
|
|
newPositionNotificationCardEl: "new-position-notification-card",
|
|
};
|
|
}
|
|
|
|
connectedCallback() {
|
|
super.connectedCallback();
|
|
if (this.initialized) {
|
|
return;
|
|
}
|
|
this.initialized = true;
|
|
this.showHeader =
|
|
!RPMGetBoolPref(INTEGRATED_SIDEBAR_PREF) ||
|
|
RPMGetBoolPref(SIDEBAR_REVAMP_PREF);
|
|
|
|
window.document.addEventListener("Update", this);
|
|
window.document.addEventListener("NewAnalysisRequested", this);
|
|
window.document.addEventListener("ReanalysisRequested", this);
|
|
window.document.addEventListener("ReportedProductAvailable", this);
|
|
window.document.addEventListener("adsEnabledByUserChanged", this);
|
|
window.document.addEventListener("scroll", this);
|
|
window.document.addEventListener("UpdateRecommendations", this);
|
|
window.document.addEventListener("UpdateAnalysisProgress", this);
|
|
window.document.addEventListener("autoOpenEnabledByUserChanged", this);
|
|
window.document.addEventListener("ShowKeepClosedMessage", this);
|
|
window.document.addEventListener("HideKeepClosedMessage", this);
|
|
window.document.addEventListener("ShowNewPositionCard", this);
|
|
window.document.addEventListener("HideNewPositionCard", this);
|
|
|
|
window.dispatchEvent(
|
|
new CustomEvent("ContentReady", {
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
disconnectedCallback() {
|
|
this.headerResizeObserver?.disconnect();
|
|
}
|
|
|
|
firstUpdated() {
|
|
this.headerResizeObserver = new ResizeObserver(([entry]) =>
|
|
this.maybeSetIsHeaderOverflow(entry)
|
|
);
|
|
this.headerResizeObserver.observe(this.header);
|
|
}
|
|
|
|
updated(changedProperties) {
|
|
if (changedProperties.has("supportedDomains")) {
|
|
let oldVal = changedProperties.get("supportedDomains");
|
|
/**
|
|
* We expect the domains object to be passed in consistently and not change often.
|
|
* A shallow comparison seems to be enough.
|
|
*/
|
|
try {
|
|
if (JSON.stringify(oldVal) !== JSON.stringify(this.supportedDomains)) {
|
|
// Let the render function deal with recreating the formatted list.
|
|
this.formattedDomainList = null;
|
|
}
|
|
} catch (e) {
|
|
console.error(e);
|
|
this.formattedDomainList = null;
|
|
}
|
|
}
|
|
|
|
if (this.focusCloseButton) {
|
|
this.closeButtonEl.focus();
|
|
}
|
|
}
|
|
|
|
async _update({
|
|
data,
|
|
showOnboarding,
|
|
productUrl,
|
|
recommendationData,
|
|
adsEnabled,
|
|
adsEnabledByUser,
|
|
isAnalysisInProgress,
|
|
analysisProgress,
|
|
focusCloseButton,
|
|
autoOpenEnabled,
|
|
autoOpenEnabledByUser,
|
|
isProductPage,
|
|
isSupportedSite,
|
|
supportedDomains,
|
|
isSidebarStartPosition,
|
|
}) {
|
|
// If we're not opted in or there's no shopping URL in the main browser,
|
|
// the actor will pass `null`, which means this will clear out any existing
|
|
// content in the sidebar.
|
|
this.data = data;
|
|
this.showOnboarding = showOnboarding ?? this.showOnboarding;
|
|
this.productUrl = productUrl;
|
|
this.recommendationData = recommendationData;
|
|
this.isOffline = !navigator.onLine;
|
|
this.isAnalysisInProgress = isAnalysisInProgress;
|
|
this.adsEnabled = adsEnabled ?? this.adsEnabled;
|
|
this.adsEnabledByUser = adsEnabledByUser ?? this.adsEnabledByUser;
|
|
this.analysisProgress = analysisProgress;
|
|
this.focusCloseButton = focusCloseButton;
|
|
this.autoOpenEnabled = autoOpenEnabled ?? this.autoOpenEnabled;
|
|
this.autoOpenEnabledByUser =
|
|
autoOpenEnabledByUser ?? this.autoOpenEnabledByUser;
|
|
this.isProductPage = isProductPage ?? true;
|
|
this.isSupportedSite = isSupportedSite;
|
|
this.supportedDomains = supportedDomains ?? this.supportedDomains;
|
|
this.isSidebarStartPosition =
|
|
isSidebarStartPosition ?? this.isSidebarStartPosition;
|
|
}
|
|
|
|
_updateRecommendations({ recommendationData }) {
|
|
this.recommendationData = recommendationData;
|
|
}
|
|
|
|
_updateAnalysisProgress({ progress }) {
|
|
this.analysisProgress = progress;
|
|
}
|
|
|
|
handleEvent(event) {
|
|
switch (event.type) {
|
|
case "Update":
|
|
this._update(event.detail);
|
|
break;
|
|
case "NewAnalysisRequested":
|
|
case "ReanalysisRequested":
|
|
this.isAnalysisInProgress = true;
|
|
this.analysisEvent = {
|
|
type: event.type,
|
|
productUrl: this.productUrl,
|
|
};
|
|
window.dispatchEvent(
|
|
new CustomEvent("PolledRequestMade", {
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
break;
|
|
case "ReportedProductAvailable":
|
|
this.userReportedAvailable = true;
|
|
window.dispatchEvent(
|
|
new CustomEvent("ReportProductAvailable", {
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
Glean.shopping.surfaceReactivatedButtonClicked.record();
|
|
break;
|
|
case "adsEnabledByUserChanged":
|
|
this.adsEnabledByUser = event.detail?.adsEnabledByUser;
|
|
break;
|
|
case "scroll":
|
|
this.showHeaderShadow = window.scrollY > HEADER_SCROLL_PIXEL_OFFSET;
|
|
break;
|
|
case "UpdateRecommendations":
|
|
this._updateRecommendations(event.detail);
|
|
break;
|
|
case "UpdateAnalysisProgress":
|
|
this._updateAnalysisProgress(event.detail);
|
|
break;
|
|
case "autoOpenEnabledByUserChanged":
|
|
this.autoOpenEnabledByUser = event.detail?.autoOpenEnabledByUser;
|
|
break;
|
|
case "ShowKeepClosedMessage":
|
|
this.showingKeepClosedMessage = true;
|
|
break;
|
|
case "HideKeepClosedMessage":
|
|
this.showingKeepClosedMessage = false;
|
|
break;
|
|
case "ShowNewPositionCard":
|
|
this.showNewPositionCard = true;
|
|
break;
|
|
case "HideNewPositionCard":
|
|
this.showNewPositionCard = false;
|
|
break;
|
|
}
|
|
}
|
|
|
|
maybeSetIsHeaderOverflow(entry) {
|
|
let isOverflow = entry.contentRect.height > HEADER_NOT_TEXT_WRAPPED_HEIGHT;
|
|
if (this.isHeaderOverflow != isOverflow) {
|
|
this.isHeaderOverflow = isOverflow;
|
|
}
|
|
}
|
|
|
|
getHostnameFromProductUrl() {
|
|
let hostname = URL.parse(this.productUrl)?.hostname;
|
|
if (hostname) {
|
|
return hostname;
|
|
}
|
|
console.warn(`Unknown product url ${this.productUrl}.`);
|
|
return null;
|
|
}
|
|
|
|
analysisDetailsTemplate() {
|
|
/* At present, en is supported as the default language for reviews. As we support more sites,
|
|
* update `lang` accordingly if highlights need to be displayed in other languages. */
|
|
let hostname = this.getHostnameFromProductUrl();
|
|
|
|
let lang = "en";
|
|
let isDEFRSupported = RPMGetBoolPref(
|
|
"toolkit.shopping.experience2023.defr",
|
|
false
|
|
);
|
|
|
|
if (isDEFRSupported && hostname === "www.amazon.fr") {
|
|
lang = "fr";
|
|
} else if (isDEFRSupported && hostname === "www.amazon.de") {
|
|
lang = "de";
|
|
}
|
|
|
|
return html`
|
|
<review-reliability letter=${this.data.grade}></review-reliability>
|
|
<adjusted-rating
|
|
rating=${ifDefined(this.data.adjusted_rating)}
|
|
></adjusted-rating>
|
|
<review-highlights
|
|
.highlights=${this.data.highlights}
|
|
lang=${lang}
|
|
></review-highlights>
|
|
`;
|
|
}
|
|
|
|
hasDataTemplate() {
|
|
let dataBodyTemplate = null;
|
|
|
|
// The user requested an analysis which is not done yet.
|
|
if (
|
|
this.analysisEvent?.productUrl == this.productUrl &&
|
|
this.isAnalysisInProgress
|
|
) {
|
|
const isReanalysis = this.analysisEvent.type === "ReanalysisRequested";
|
|
dataBodyTemplate = html`<shopping-message-bar
|
|
type=${isReanalysis
|
|
? "reanalysis-in-progress"
|
|
: "analysis-in-progress"}
|
|
progress=${this.analysisProgress}
|
|
></shopping-message-bar>
|
|
${isReanalysis ? this.analysisDetailsTemplate() : null}`;
|
|
} else if (this.data?.error) {
|
|
dataBodyTemplate = html`<shopping-message-bar
|
|
type="generic-error"
|
|
></shopping-message-bar>`;
|
|
} else if (this.data.page_not_supported) {
|
|
dataBodyTemplate = html`<shopping-message-bar
|
|
type="page-not-supported"
|
|
></shopping-message-bar>`;
|
|
} else if (this.data.deleted_product_reported) {
|
|
dataBodyTemplate = html`<shopping-message-bar
|
|
type="product-not-available-reported"
|
|
></shopping-message-bar>`;
|
|
} else if (this.data.deleted_product) {
|
|
dataBodyTemplate = this.userReportedAvailable
|
|
? html`<shopping-message-bar
|
|
type="thanks-for-reporting"
|
|
></shopping-message-bar>`
|
|
: html`<shopping-message-bar
|
|
type="product-not-available"
|
|
></shopping-message-bar>`;
|
|
} else if (this.data.needs_analysis) {
|
|
if (!this.data.product_id || typeof this.data.grade != "string") {
|
|
// Product is new to us.
|
|
dataBodyTemplate = html`<unanalyzed-product-card
|
|
productUrl=${ifDefined(this.productUrl)}
|
|
></unanalyzed-product-card>`;
|
|
} else {
|
|
// We successfully analyzed the product before, but the current analysis is outdated and can be updated
|
|
// via a re-analysis.
|
|
dataBodyTemplate = html`
|
|
<shopping-message-bar
|
|
type="stale"
|
|
.productUrl=${this.productUrl}
|
|
></shopping-message-bar>
|
|
${this.analysisDetailsTemplate()}
|
|
`;
|
|
}
|
|
} else if (this.data.not_enough_reviews) {
|
|
// We already saw and tried to analyze this product before, but there are not enough reviews
|
|
// to make a detailed analysis.
|
|
dataBodyTemplate = html`<shopping-message-bar
|
|
type="not-enough-reviews"
|
|
></shopping-message-bar>`;
|
|
} else {
|
|
dataBodyTemplate = this.analysisDetailsTemplate();
|
|
}
|
|
|
|
return html`
|
|
${dataBodyTemplate}${this.explainerTemplate()}${this.recommendationTemplate()}
|
|
`;
|
|
}
|
|
|
|
recommendationTemplate() {
|
|
const canShowAds = this.adsEnabled && this.adsEnabledByUser;
|
|
if (this.recommendationData?.length && canShowAds) {
|
|
return html`<recommended-ad
|
|
.product=${this.recommendationData[0]}
|
|
></recommended-ad>`;
|
|
}
|
|
return null;
|
|
}
|
|
|
|
/**
|
|
* @param {object?} options
|
|
* @param {boolean?} options.animate = true
|
|
* Whether to animate the loading state. Defaults to true.
|
|
* There will be no animation for users who prefer reduced motion,
|
|
* irrespective of the value of this option.
|
|
*/
|
|
loadingTemplate({ animate = true } = {}) {
|
|
/* Due to limitations with aria-busy for certain screen readers
|
|
* (see Bug 1682063), mark loading container as a pseudo image and
|
|
* use aria-label as a workaround. */
|
|
return html`
|
|
<div
|
|
id="loading-wrapper"
|
|
data-l10n-id="shopping-a11y-loading"
|
|
role="img"
|
|
class=${animate ? "animate" : ""}
|
|
>
|
|
<div class="loading-box medium"></div>
|
|
<div class="loading-box medium"></div>
|
|
<div class="loading-box large"></div>
|
|
<div class="loading-box small"></div>
|
|
<div class="loading-box large"></div>
|
|
<div class="loading-box small"></div>
|
|
</div>
|
|
`;
|
|
}
|
|
|
|
noDataTemplate({ animate = true } = {}) {
|
|
if (this.isAnalysisInProgress) {
|
|
return html`<shopping-message-bar
|
|
type="analysis-in-progress"
|
|
progress=${this.analysisProgress}
|
|
></shopping-message-bar
|
|
>${this.explainerTemplate()}${this.recommendationTemplate()}`;
|
|
}
|
|
return this.loadingTemplate({ animate });
|
|
}
|
|
|
|
nonPDPTemplate() {
|
|
// TODO: (Bug 1937924) settings template will throw a warning since this.productUrl is null when viewing a non-PDP
|
|
const bodyTextTemplate = this.isSupportedSite
|
|
? html` <p
|
|
id="shopping-empty-state-text"
|
|
data-l10n-id="shopping-empty-state-supported-site"
|
|
></p>`
|
|
: html`
|
|
<p
|
|
id="shopping-empty-state-text"
|
|
data-l10n-id="shopping-empty-state-non-supported-site"
|
|
></p>
|
|
`;
|
|
|
|
if (!this.formattedDomainList) {
|
|
this.formattedDomainList = this._formattedDomainListTemplate();
|
|
}
|
|
|
|
const listTemplate = !this.isSupportedSite
|
|
? html`<ul id="shopping-empty-list-of-supported-domains">
|
|
${this.formattedDomainList}
|
|
</ul>`
|
|
: null;
|
|
|
|
// Anything wrapped by #shopping-empty-wrapper will be centered on sidebar width changes.
|
|
return html`<div id="shopping-empty-wrapper">
|
|
<img
|
|
id="shopping-empty-state-img"
|
|
src="chrome://browser/content/shopping/assets/emptyStateA.svg"
|
|
alt=""
|
|
role="presentation"
|
|
/>
|
|
<h2
|
|
id="shopping-empty-state-header"
|
|
data-l10n-id="shopping-empty-state-header"
|
|
></h2>
|
|
${bodyTextTemplate} ${listTemplate}
|
|
</div>
|
|
${this.isSupportedSite
|
|
? this.explainerTemplate({ className: "first-footer-card" })
|
|
: null}`;
|
|
}
|
|
|
|
_formattedDomainListTemplate() {
|
|
if (!this.supportedDomains) {
|
|
return null;
|
|
}
|
|
|
|
let template = [];
|
|
let formatter = new Intl.ListFormat(undefined, {
|
|
style: "narrow",
|
|
type: "conjunction",
|
|
});
|
|
|
|
Object.keys(this.supportedDomains)
|
|
.sort()
|
|
.forEach(sitename => {
|
|
let domainsFromSite = this.supportedDomains[sitename];
|
|
|
|
// List of supported domains per sitename, per row, as a string.
|
|
let anchorsString = domainsFromSite.map(domain => {
|
|
try {
|
|
let url = new URL(domain);
|
|
let hostname = url.hostname;
|
|
/** ShoppingProduct should already validate the URLs in the ProductConfig for us.
|
|
* As an extra precaution though, let's verify that we've been passed a valid URL, in case
|
|
* something goes awry in messages between actors and the shopping-container.
|
|
*
|
|
* @see ShoppingProduct
|
|
*/
|
|
let validProtocolRegex = /^(https:\/\/\w+.*)/;
|
|
return validProtocolRegex.test(url)
|
|
? `<a class="shopping-supported-domain-link" href=${url.href} target="_blank">${hostname}</a>`
|
|
: null;
|
|
} catch (e) {
|
|
// Somehow, we got an invalid URL.
|
|
console.error(e);
|
|
return null;
|
|
}
|
|
});
|
|
|
|
// Now format the string as a list suitable for the current locale.
|
|
anchorsString = formatter.format(anchorsString);
|
|
|
|
// Convert the formatted string into an element that can be inserted into our litElement template.
|
|
const parser = new DOMParser();
|
|
let anchorsDOMDoc = parser.parseFromString(anchorsString, "text/html");
|
|
/**
|
|
* litElement will lose a reference to the childNodes on re-render if we use a DocumentFragment.
|
|
* Instead, add the nodes as an array in our template so that we preserve them.
|
|
*/
|
|
let anchorsTemplate = [];
|
|
Array.from(anchorsDOMDoc.body.childNodes).forEach(childNode => {
|
|
anchorsTemplate.push(childNode);
|
|
});
|
|
|
|
let listTemplate = html` <li
|
|
id="shopping-empty-state-domains-list-${sitename}"
|
|
class="shopping-supported-domain-list"
|
|
>
|
|
${anchorsTemplate}
|
|
</li>`;
|
|
|
|
template.push(listTemplate);
|
|
});
|
|
|
|
return template;
|
|
}
|
|
|
|
headerTemplate() {
|
|
const headerWrapperClasses = `${this.showHeaderShadow ? "header-wrapper-shadow" : ""} ${this.isHeaderOverflow ? "header-wrapper-overflow" : ""}`;
|
|
return html`<div id="header-wrapper" class=${headerWrapperClasses}>
|
|
<header id="shopping-header" data-l10n-id="shopping-a11y-header">
|
|
<h1
|
|
id="shopping-header-title"
|
|
data-l10n-id="shopping-main-container-title"
|
|
></h1>
|
|
<p id="beta-marker" data-l10n-id="shopping-beta-marker"></p>
|
|
</header>
|
|
<button
|
|
id="close-button"
|
|
class="ghost-button shopping-button"
|
|
data-l10n-id="shopping-close-button"
|
|
@click=${this.handleCloseButtonClick}
|
|
></button>
|
|
</div>`;
|
|
}
|
|
|
|
// TODO: (Bug 1949647) do not render "Keep closed" message and notification card simultaneously.
|
|
renderContainer(sidebarContent, { showSettings = false } = {}) {
|
|
/* Empty state styles for users that are not yet opted-in are managed separately
|
|
* by AboutWelcomeChild.sys.mjs and _shopping.scss. To prevent overlap, only apply
|
|
* the class for these styles if a user is opted-in. */
|
|
const canStyleEmptyState =
|
|
!this.isProductPage && !this.isOffline && !this.showOnboarding;
|
|
return html`<link
|
|
rel="stylesheet"
|
|
href="chrome://browser/content/shopping/shopping-container.css"
|
|
/>
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://global/skin/in-content/common.css"
|
|
/>
|
|
<link
|
|
rel="stylesheet"
|
|
href="chrome://browser/content/shopping/shopping-page.css"
|
|
/>
|
|
<div id="shopping-container">
|
|
${this.showHeader ? this.headerTemplate() : null}
|
|
<div
|
|
id="content"
|
|
class=${canStyleEmptyState ? "is-empty-state" : ""}
|
|
aria-live="polite"
|
|
aria-busy=${!this.data}
|
|
>
|
|
<slot name="multi-stage-message-slot"></slot>
|
|
${this.userInteractionMessageTemplate()}${sidebarContent}
|
|
${showSettings
|
|
? this.settingsTemplate(
|
|
!this.isSupportedSite && !this.isProductPage
|
|
? { className: "first-footer-card" }
|
|
: ""
|
|
)
|
|
: null}
|
|
</div>
|
|
</div>`;
|
|
}
|
|
|
|
explainerTemplate({ className = "" } = {}) {
|
|
return html`<analysis-explainer
|
|
productUrl=${ifDefined(this.productUrl)}
|
|
class=${className}
|
|
></analysis-explainer>`;
|
|
}
|
|
|
|
settingsTemplate({ className = "" } = {}) {
|
|
let hostname = this.getHostnameFromProductUrl();
|
|
return html` <shopping-settings
|
|
?adsEnabled=${this.adsEnabled}
|
|
?adsEnabledByUser=${this.adsEnabledByUser}
|
|
?autoOpenEnabled=${this.autoOpenEnabled}
|
|
?autoOpenEnabledByUser=${this.autoOpenEnabledByUser}
|
|
.hostname=${hostname}
|
|
class=${className}
|
|
></shopping-settings>`;
|
|
}
|
|
|
|
userInteractionMessageTemplate() {
|
|
/**
|
|
* There are two types of messages about users' interaction with Review Checker that we want to display
|
|
* when users keep the auto-open setting enabled:
|
|
* 1. The "Keep closed" message-bar
|
|
* 2. The "New position" notification card (integratedSidebar only)
|
|
*
|
|
* Only one or the other should be rendered at a time, at the same spot. If a user is eligible
|
|
* to see the notification card, make sure to show that card first. Once the card is dismissed,
|
|
* we can then check if the user is eligible to see the "Keep closed" message.
|
|
*/
|
|
if (!this.autoOpenEnabled || !this.autoOpenEnabledByUser) {
|
|
return null;
|
|
}
|
|
|
|
let canShowNotificationCard =
|
|
RPMGetBoolPref(INTEGRATED_SIDEBAR_PREF, false) &&
|
|
this.showNewPositionCard &&
|
|
this.isSidebarStartPosition &&
|
|
// Set fallback value to true to prevent weird flickering UI when switching tabs
|
|
!RPMGetBoolPref(HAS_SEEN_POSITION_NOTIFICATION_CARD_PREF, true) &&
|
|
this.isProductPage;
|
|
let canShowKeepClosedMessage =
|
|
this.showingKeepClosedMessage && this.isProductPage;
|
|
|
|
if (canShowNotificationCard) {
|
|
return this.newPositionNotificationCardTemplate();
|
|
} else if (canShowKeepClosedMessage) {
|
|
return this.keepClosedMessageTemplate();
|
|
}
|
|
|
|
return null;
|
|
}
|
|
|
|
newPositionNotificationCardTemplate() {
|
|
return html`
|
|
<new-position-notification-card
|
|
isSidebarStartPosition=${this.isSidebarStartPosition}
|
|
></new-position-notification-card>
|
|
`;
|
|
}
|
|
|
|
keepClosedMessageTemplate() {
|
|
return html`<shopping-message-bar
|
|
id="keep-closed-message-bar"
|
|
type="keep-closed"
|
|
></shopping-message-bar>`;
|
|
}
|
|
|
|
render() {
|
|
let content;
|
|
// this.data may be null because we're viewing a non PDP, or a PDP that does not have data yet.
|
|
// Use isProductPage and isSupported to distinguish between the two.
|
|
const isLoadingData =
|
|
!this.data && this.isProductPage && !this.isSupportedSite;
|
|
|
|
if (this.showOnboarding) {
|
|
content = html``;
|
|
} else if (isLoadingData || this.isOffline) {
|
|
content = this.noDataTemplate({ animate: !this.isOffline });
|
|
} else if (!this.isProductPage || this.isSupportedSite) {
|
|
content = this.nonPDPTemplate();
|
|
} else {
|
|
content = this.hasDataTemplate();
|
|
}
|
|
|
|
const showSettings =
|
|
!this.showOnboarding &&
|
|
!this.isOffline &&
|
|
(!isLoadingData || (isLoadingData && this.isAnalysisInProgress));
|
|
return this.renderContainer(content, { showSettings });
|
|
}
|
|
|
|
handleCloseButtonClick() {
|
|
let canShowKeepClosedMessage;
|
|
|
|
if (this.autoOpenEnabled && this.autoOpenEnabledByUser) {
|
|
canShowKeepClosedMessage =
|
|
this._canShowKeepClosedMessageOnCloseButtonClick();
|
|
}
|
|
|
|
if (!canShowKeepClosedMessage) {
|
|
RPMSetPref(SHOPPING_SIDEBAR_ACTIVE_PREF, false);
|
|
window.dispatchEvent(
|
|
new CustomEvent("CloseShoppingSidebar", {
|
|
bubbles: true,
|
|
composed: true,
|
|
})
|
|
);
|
|
}
|
|
|
|
Glean.shopping.surfaceClosed.record({ source: "closeButton" });
|
|
}
|
|
|
|
/**
|
|
* Delaying close is only applicable to the "Keep closed" message.
|
|
*
|
|
* We can show the message under these conditions:
|
|
* - User has already seen the new location notification card
|
|
* - The message was never seen before
|
|
* - User clicked the close button at least 5 times in a session (i.e. met the minimum of 4 counts before this point)
|
|
* - The number of close attempts in a session is less than 7 (i.e. met the maximum of 6 counts before this point)
|
|
*
|
|
* Do not show the message again after the 7th close.
|
|
*/
|
|
_canShowKeepClosedMessageOnCloseButtonClick() {
|
|
let yetToSeeNotificationCard =
|
|
!RPMGetBoolPref(HAS_SEEN_POSITION_NOTIFICATION_CARD_PREF, false) &&
|
|
RPMGetBoolPref(INTEGRATED_SIDEBAR_PREF, false);
|
|
|
|
if (
|
|
yetToSeeNotificationCard ||
|
|
!RPMGetBoolPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false) ||
|
|
this.showOnboarding ||
|
|
!this.isProductPage
|
|
) {
|
|
return false;
|
|
}
|
|
|
|
let sidebarClosedCount = RPMGetIntPref(SIDEBAR_CLOSED_COUNT_PREF, 0);
|
|
let canShowKeepClosedMessage =
|
|
!this.showingKeepClosedMessage &&
|
|
sidebarClosedCount >= CLOSED_COUNT_PREVIOUS_MIN;
|
|
|
|
if (canShowKeepClosedMessage) {
|
|
this.showingKeepClosedMessage = true;
|
|
return true;
|
|
}
|
|
|
|
this.showingKeepClosedMessage = false;
|
|
|
|
if (sidebarClosedCount >= CLOSED_COUNT_PREVIOUS_MAX) {
|
|
RPMSetPref(SHOW_KEEP_SIDEBAR_CLOSED_MESSAGE_PREF, false);
|
|
} else {
|
|
RPMSetPref(SIDEBAR_CLOSED_COUNT_PREF, sidebarClosedCount + 1);
|
|
}
|
|
|
|
return false;
|
|
}
|
|
}
|
|
|
|
customElements.define("shopping-container", ShoppingContainer);
|