Files
tubestation/browser/components/shopping/ReviewCheckerManager.sys.mjs

528 lines
16 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/. */
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
isProductURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
isSupportedSiteURL: "chrome://global/content/shopping/ShoppingProduct.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
ShoppingUtils: "resource:///modules/ShoppingUtils.sys.mjs",
});
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"optedIn",
"browser.shopping.experience2023.optedIn",
null
);
XPCOMUtils.defineLazyPreferenceGetter(
lazy,
"hasSeenNewPositionCard",
"browser.shopping.experience2023.newPositionCard.hasSeen",
null
);
const AUTO_ACTIVATE_COUNT_PREF =
"browser.shopping.experience2023.autoActivateCount";
const ONBOARDING_FIRST_IMPRESSION_TIME_PREF =
"browser.shopping.experience2023.firstImpressionTime";
/**
* Manages opening and closing the Review Checker in the sidebar.
* Listens for location changes and will auto-open if enabled.
*/
export class ReviewCheckerManager {
static SIDEBAR_ID = "viewReviewCheckerSidebar";
static SIDEBAR_TOOL = "reviewchecker";
static SIDEBAR_ENABLED_PREF = "reviewchecker";
static SIDEBAR_SETTINGS_ID = "viewCustomizeSidebar";
static SHOW_NEW_POSITION_NOTIFICATION_CARD_EVENT_NAME =
"ReviewCheckerManager:ShowNewPositionCard";
#enabled = false;
#hasListeners = null;
#didAutoOpenForOptedInUser = false;
#currentURI = null;
/**
* Creates manager to open and close the review checker sidebar
* in a passed window.
*
* @param {Window} win
*/
constructor(win) {
let isPBM = lazy.PrivateBrowsingUtils.isWindowPrivate(win);
if (isPBM) {
return;
}
this.window = win;
this.SidebarController = win.SidebarController;
this.init();
}
/**
* Start listening for changes to the
* enabled sidebar tools.
*/
init() {
XPCOMUtils.defineLazyPreferenceGetter(
this,
"sidebarTools",
"sidebar.main.tools",
null,
() => {
this.#updateSidebarEnabled();
}
);
this.#updateSidebarEnabled();
}
/**
* Remove any listeners.
*/
uninit() {
this.#removeListeners();
this.#didAutoOpenForOptedInUser = null;
this.#currentURI = null;
}
get optedIn() {
return lazy.optedIn;
}
get hasSeenNewPositionCard() {
return lazy.hasSeenNewPositionCard;
}
#updateSidebarEnabled() {
// Add listeners if "reviewchecker" is enabled in the sidebar tools.
this.#enabled = this.sidebarTools.includes(
ReviewCheckerManager.SIDEBAR_TOOL
);
if (this.#enabled) {
this.#addListeners();
} else {
this.#removeListeners();
}
}
#addListeners() {
if (this.#hasListeners) {
return;
}
this.window.gBrowser.addTabsProgressListener(this);
this.window.gBrowser.tabContainer.addEventListener("TabSelect", this);
this.window.addEventListener("OpenReviewCheckerSidebar", this);
this.window.addEventListener("CloseReviewCheckerSidebar", this);
this.window.addEventListener("DispatchNewPositionCardIfEligible", this);
this.window.addEventListener(
"ReverseSidebarPositionFromReviewChecker",
this
);
this.window.addEventListener("ShowSidebarSettingsFromReviewChecker", this);
this.window.addEventListener("SidebarShown", this);
this.window.addEventListener("SidebarWillHide", this);
this.#hasListeners = true;
}
#removeListeners() {
if (!this.#hasListeners) {
return;
}
this.window.gBrowser.removeTabsProgressListener(this);
this.window.gBrowser.tabContainer.removeEventListener("TabSelect", this);
this.window.removeEventListener("OpenReviewCheckerSidebar", this);
this.window.removeEventListener("CloseReviewCheckerSidebar", this);
this.window.removeEventListener("DispatchNewPositionCardIfEligible", this);
this.window.removeEventListener(
"ReverseSidebarPositionFromReviewChecker",
this
);
this.window.removeEventListener(
"ShowSidebarSettingsFromReviewChecker",
this
);
this.#hasListeners = null;
}
/**
* Show the Review Checker in the sidebar for the managed window.
*/
showSidebar() {
let { selectedBrowser } = this.window.gBrowser;
lazy.ShoppingUtils.clearWasClosedFlag(selectedBrowser);
this.SidebarController.show(ReviewCheckerManager.SIDEBAR_ID);
}
/**
* Hide the sidebar for the managed window if the Review Checker
* is currently shown.
*
* @param {boolean} autoClosed
* true if the sidebar was auto-closed instead
* of by a user action.
*/
hideSidebar(autoClosed) {
if (
!this.SidebarController.isOpen ||
this.SidebarController.currentID !== ReviewCheckerManager.SIDEBAR_ID
) {
return;
}
// Prevent auto-opening this product again on tab switch.
if (!autoClosed && this.#canAutoOpen) {
let { selectedBrowser } = this.window.gBrowser;
selectedBrowser.reviewCheckerWasClosed = true;
}
this.SidebarController.hide();
}
/**
* Checks if the sidebar is open to the Review Checker.
*/
isSidebarOpen() {
return (
this.SidebarController.isOpen &&
this.SidebarController.currentID == ReviewCheckerManager.SIDEBAR_ID
);
}
/**
* Reverses the position of the sidebar. If the sidebar is positioned on the left, it will
* move to the right, and vice-versa.
*/
reverseSidebarPositon() {
if (
!this.SidebarController.isOpen ||
this.SidebarController.currentID !== ReviewCheckerManager.SIDEBAR_ID
) {
return;
}
this.SidebarController.reversePosition();
}
/**
* Displays the sidebar's settings panel.
*/
showSidebarSettings() {
if (
!this.SidebarController.isOpen ||
this.SidebarController.currentID !== ReviewCheckerManager.SIDEBAR_ID
) {
return;
}
this.SidebarController.show(ReviewCheckerManager.SIDEBAR_SETTINGS_ID);
}
#canAutoOpen() {
let isEligible = lazy.ShoppingUtils.isAutoOpenEligible();
return isEligible && this.#enabled;
}
#canShowNewPositionCard() {
return this.optedIn && !this.hasSeenNewPositionCard;
}
#shouldAutoActivate() {
let shouldAutoActivate = false;
let autoActivateCount = Services.prefs.getIntPref(
AUTO_ACTIVATE_COUNT_PREF,
0
);
if (autoActivateCount == 0 && !this.optedIn) {
shouldAutoActivate = true;
}
return shouldAutoActivate;
}
/*
* Records auto-activation of the Review Checker panel
*/
#recordAutoActivation() {
const now = Date.now() / 1000;
Services.prefs.setIntPref(ONBOARDING_FIRST_IMPRESSION_TIME_PREF, now);
let autoActivateCount = Services.prefs.getIntPref(
AUTO_ACTIVATE_COUNT_PREF,
0
);
Services.prefs.setIntPref(AUTO_ACTIVATE_COUNT_PREF, autoActivateCount + 1);
}
/**
* Called by onLocationChange or onTabSelect to handle navigations
* and tab switches.
*
* This is called on navigations in the current tab or as a
* simulated change when switching tabs.
*
* Updates ShoppingUtils with the location changes.
*
* If a new location is a product url this will:
* - Check if the sidebar should be auto-opened.
* - Show the sidebar if needed.
*
* @param {nsIURI} aLocationURI
* The URI of the new location.
* @param {integer} aFlags
* The reason for the location change.
* @param {boolean} aIsSimulated
* True when change is from switching tabs
* and undefined otherwise.
*/
locationChangeHandler(aLocationURI, aFlags, aIsSimulated) {
this.#didAutoOpenForOptedInUser = false;
let previousURI = this.#currentURI;
let isSupportedSite = lazy.isSupportedSiteURL(aLocationURI);
let isProductURL = lazy.isProductURL(aLocationURI);
let { selectedBrowser } = this.window.gBrowser;
// Only product URIs are needed for comparison.
this.#currentURI = isProductURL ? aLocationURI : null;
// Close the sidebar on tab switches if user has previously closed
// the sidebar in this tab (for this location).
let wasClosed = aIsSimulated && selectedBrowser.reviewCheckerWasClosed;
let canAutoClose = this.#canAutoOpen() && !isSupportedSite && !isProductURL;
if (canAutoClose || wasClosed) {
this.hideSidebar(true);
return;
}
// Check if this is a new location.
let hasLocationChanged = lazy.ShoppingUtils.hasLocationChanged(
aLocationURI,
aFlags,
previousURI
);
// Only auto-open the sidebar for a product page.
if (!isProductURL || !hasLocationChanged) {
return;
}
// Allow this tab to auto-open again if this was a navigation
// to a new product.
if (!aIsSimulated) {
lazy.ShoppingUtils.clearWasClosedFlag(selectedBrowser);
}
// If the sidebar is currently open there is no need to auto-open,
// and the ReviewCheckerParent will handle recording the exposure.
let isSidebarOpen = this.isSidebarOpen();
if (isSidebarOpen) {
return;
}
let shouldAutoOpen = false;
let shouldAutoActivate = false;
if (this.optedIn) {
/**
* We can auto-open the sidebar if:
* - Auto-open is enabled.
* - This sidebar has not been closed for this product in this tab.
* - This is a navigation to a product page, or the location change
* is from switching to this tab.
* - The user has not had a chance to opt-in or out yet.
*/
shouldAutoOpen = this.#canAutoOpen() && !wasClosed;
} else {
// Check if we should auto-open to allow opting in.
shouldAutoActivate = this.#shouldAutoActivate();
// Only trigger the callout if the panel is not auto-opening
if (!shouldAutoOpen) {
lazy.ShoppingUtils.sendTrigger({
browser: selectedBrowser,
id: "shoppingProductPageWithIntegratedRCSidebarClosed",
context: {
isReviewCheckerInSidebarClosed: !this.SidebarController?.isOpen,
},
});
}
}
// Only show sidebar if no sidebar panel is currently showing,
// auto open is enabled and the RC sidebar is enabled.
if (
!this.SidebarController.isOpen &&
(shouldAutoOpen || shouldAutoActivate)
) {
this.showSidebar();
if (this.optedIn) {
this.#didAutoOpenForOptedInUser = true;
} else if (shouldAutoActivate) {
// set auto-activate pref
this.#recordAutoActivation();
}
return;
}
// Record a product exposure for the location change,
// if the sidebar is not going to open and record it.
if (!aIsSimulated) {
lazy.ShoppingUtils.recordExposure(aLocationURI, aFlags);
lazy.ShoppingUtils.clearIsDistinctProductPageVisitFlag(selectedBrowser);
}
}
/**
* Listens to TabsProgressListener and handles new top level
* location changes in any browser in the window.
*
* Calls locationChangeHandler with the location changes.
*
* If a background tab navigated to a location that is a product url,
* this will mark the browser with `isDistinctProductPageVisit=true`
* so that navigation can be handled when the background tab is selected.
*
* @param {Browser} aBrowser
* The browser the location change took place in.
* @param {nsIWebProgress} aWebProgress
* The nsIWebProgress instance that fired the notification.
* @param {nsIRequest} _aRequest
* Unused and is null when change is simulated.
* @param {nsIURI} aLocationURI
* The URI of the new location.
* @param {integer} aFlags
* The reason for the location change.
*/
onLocationChange(aBrowser, aWebProgress, _aRequest, aLocationURI, aFlags) {
if (!aWebProgress.isTopLevel) {
return;
}
let { selectedBrowser } = this.window.gBrowser;
// No need to handle background navigations until they
// are foregrounded.
if (aBrowser != selectedBrowser) {
let isProductURL = lazy.isProductURL(aLocationURI);
if (isProductURL) {
// Mark this product page to be handled in the future.
aBrowser.isDistinctProductPageVisit = true;
} else {
// Otherwise this is no longer a product page that
// needs to be handled.
lazy.ShoppingUtils.clearIsDistinctProductPageVisitFlag(selectedBrowser);
}
return;
}
this.locationChangeHandler(aLocationURI, aFlags, false);
}
/**
* Listens for changes to the selected tab.
*
* Calls locationChangeHandler with URI of the current tab.
*
* If this tabs location changed when not selected, the location will be
* handled like a navigation and `isDistinctProductPageVisit` will be removed
* by the ReviewCheckerParent after the sidebar has been updated.
*
* Otherwise the change will be sent as a simulated change to represent that it
* is just a tab switch as `gBrowser.onProgressListener` does.
*/
onTabSelect() {
let { selectedBrowser, currentURI } = this.window.gBrowser;
let isSimulated = !selectedBrowser.isDistinctProductPageVisit;
this.#currentURI = null;
this.locationChangeHandler(currentURI, null, isSimulated);
}
handleEvent(event) {
switch (event.type) {
case "OpenReviewCheckerSidebar": {
this.showSidebar();
break;
}
case "CloseReviewCheckerSidebar": {
this.hideSidebar();
lazy.ShoppingUtils.sendTrigger({
browser: this.window.gBrowser,
id: "reviewCheckerSidebarClosedCallout",
context: {
isReviewCheckerInSidebarClosed: !this.SidebarController?.isOpen,
isSidebarVisible: this.SidebarController._state?.launcherVisible,
},
});
break;
}
case "ReverseSidebarPositionFromReviewChecker": {
this.reverseSidebarPositon();
break;
}
case "ShowSidebarSettingsFromReviewChecker": {
this.showSidebarSettings();
break;
}
case "DispatchNewPositionCardIfEligible": {
/* Do not show the card if:
* - the Review Checker panel was not opened via auto-open on a product page,
* - if the user is not opted-in,
* - or if the card was already displayed before. */
if (
!this.#didAutoOpenForOptedInUser ||
!this.#canShowNewPositionCard()
) {
return;
}
let showNewPositionCardEvent = new CustomEvent(
ReviewCheckerManager.SHOW_NEW_POSITION_NOTIFICATION_CARD_EVENT_NAME,
{
bubbles: true,
composed: true,
}
);
this.window.dispatchEvent(showNewPositionCardEvent);
break;
}
case "TabSelect": {
this.onTabSelect();
break;
}
case "SidebarShown": {
// Clear that the sidebar was closed as it has opened
// to the ReviewChecker again.
if (
this.SidebarController.isOpen ||
this.SidebarController.currentID == ReviewCheckerManager.SIDEBAR_ID
) {
let { selectedBrowser } = this.window.gBrowser;
lazy.ShoppingUtils.clearWasClosedFlag(selectedBrowser);
}
break;
}
case "SidebarWillHide": {
lazy.ShoppingUtils.sendTrigger({
browser: this.browser,
id: "reviewCheckerSidebarClosedCallout",
context: {
isReviewCheckerInSidebarClosed: !this.SidebarController?.isOpen,
isSidebarVisible: this.SidebarController.launcherVisible,
},
});
}
}
}
}