blob: 2566b9e63566bbc4f49681faa8e7ca4252cd86de [file] [log] [blame]
// Copyright 2023 The Chromium Authors
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
import '../module_header.js';
import './suggest_tile.js';
import {CrLazyRenderElement} from 'chrome://resources/cr_elements/cr_lazy_render/cr_lazy_render.js';
import {assert} from 'chrome://resources/js/assert_ts.js';
import {listenOnce} from 'chrome://resources/js/util_ts.js';
import {PolymerElement} from 'chrome://resources/polymer/v3_0/polymer/polymer_bundled.min.js';
import {Cart} from '../../cart.mojom-webui.js';
import {Cluster, URLVisit} from '../../history_cluster_types.mojom-webui.js';
import {I18nMixin, loadTimeData} from '../../i18n_setup.js';
import {NewTabPageProxy} from '../../new_tab_page_proxy.js';
import {InfoDialogElement} from '../info_dialog';
import {ModuleDescriptor} from '../module_descriptor.js';
import {HistoryClustersProxyImpl} from './history_clusters_proxy.js';
import {getTemplate} from './module.html.js';
import {TileModuleElement} from './tile.js';
export const LAYOUT_1_MIN_IMAGE_VISITS = 2;
export const LAYOUT_1_MIN_VISITS = 2;
export const LAYOUT_2_MIN_IMAGE_VISITS = 1;
export const LAYOUT_2_MIN_VISITS = 3;
export const LAYOUT_3_MIN_IMAGE_VISITS = 2;
export const LAYOUT_3_MIN_VISITS = 4;
/**
* Available module UI layouts. This enum must match the numbering for
* NTPHistoryClustersModuleDisplayLayout in enums.xml. These values are
* persisted to logs. Entries should not be renumbered, removed or reused.
*/
export enum HistoryClusterLayoutType {
NONE = 0,
LAYOUT_1 = 1, // 2 image visits
LAYOUT_2 = 2, // 1 image visit & 2 non-image visits
LAYOUT_3 = 3, // 2 image visits & 2 non-image visits
}
/**
* Available module element types. This enum must match the numbering for
* NTPHistoryClustersElementType in enums.xml. These values are persisted to
* logs. Entries should not be renumbered, removed or reused.
*/
export enum HistoryClusterElementType {
VISIT = 0,
SUGGEST = 1,
SHOW_ALL = 2,
CART = 3,
}
/**
* The overall image presence state of the visit tiles on unloading the page.
* This enum must match the numbering for NTPHistoryClustersImageDisplayState in
* enums.xml. These values are persisted to logs. Entries should not be
* renumbered, removed or reused.
*/
export enum HistoryClusterImageDisplayState {
NONE = 0,
SOME = 1,
ALL = 2,
}
export interface HistoryClustersModuleElement {
$: {
infoDialogRender: CrLazyRenderElement<InfoDialogElement>,
};
}
export class HistoryClustersModuleElement extends I18nMixin
(PolymerElement) {
static get is() {
return 'ntp-history-clusters';
}
static get template() {
return getTemplate();
}
static get properties() {
return {
layoutType: Number,
/** The cluster displayed by this element. */
cluster: Object,
/** The cart displayed by this element, could be null. */
cart: {
type: Object,
value: null,
},
searchResultPage: Object,
};
}
cluster: Cluster;
cart: Cart|null;
layoutType: HistoryClusterLayoutType;
searchResultPage: URLVisit;
private setDisabledModulesListenerId_: number|null = null;
override ready() {
super.ready();
listenOnce(window, 'unload', () => {
const visitTiles: TileModuleElement[] = Array.from(
this.shadowRoot!.querySelectorAll('ntp-history-clusters-tile'));
const count = visitTiles.reduce(
(acc, tile) => acc + (tile.hasImageUrl() ? 1 : 0), 0);
const state = (visitTiles.length === count) ?
HistoryClusterImageDisplayState.ALL :
(count === 0) ? HistoryClusterImageDisplayState.NONE :
HistoryClusterImageDisplayState.SOME;
chrome.metricsPrivate.recordEnumerationValue(
`NewTabPage.HistoryClusters.Layout${
this.layoutType}.ImageDisplayState`,
state, Object.keys(HistoryClusterImageDisplayState).length);
});
}
override connectedCallback() {
super.connectedCallback();
if (loadTimeData.getBoolean(
'modulesChromeCartInHistoryClustersModuleEnabled')) {
this.setDisabledModulesListenerId_ =
NewTabPageProxy.getInstance()
.callbackRouter.setDisabledModules.addListener(
async (_: boolean, ids: string[]) => {
if (ids.includes('chrome_cart')) {
this.cart = null;
} else if (!this.cart) {
const {cart} =
await HistoryClustersProxyImpl.getInstance()
.handler.getCartForCluster(this.cluster);
this.cart = cart;
}
});
}
}
override disconnectedCallback() {
super.disconnectedCallback();
if (this.setDisabledModulesListenerId_) {
NewTabPageProxy.getInstance().callbackRouter.removeListener(
this.setDisabledModulesListenerId_);
}
}
private isLayout_(type: HistoryClusterLayoutType): boolean {
return type === this.layoutType;
}
private onVisitTileClick_(e: Event) {
this.recordTileClickIndex_(e.target as HTMLElement, 'Visit');
this.recordClick_(HistoryClusterElementType.VISIT);
}
private onSuggestTileClick_(e: Event) {
this.recordTileClickIndex_(e.target as HTMLElement, 'Suggest');
this.recordClick_(HistoryClusterElementType.SUGGEST);
}
private onCartTileClick_() {
this.recordClick_(HistoryClusterElementType.CART);
}
private recordClick_(type: HistoryClusterElementType) {
chrome.metricsPrivate.recordEnumerationValue(
`NewTabPage.HistoryClusters.Layout${this.layoutType}.Click`, type,
Object.keys(HistoryClusterElementType).length);
}
private recordTileClickIndex_(tile: HTMLElement, tileType: string) {
assert(this.layoutType !== HistoryClusterLayoutType.NONE);
const index = Array.from(tile.parentNode!.children).indexOf(tile);
chrome.metricsPrivate.recordValue(
{
metricName: `NewTabPage.HistoryClusters.Layout${this.layoutType}.${
tileType!}Tile.ClickIndex`,
type: chrome.metricsPrivate.MetricTypeType.HISTOGRAM_LINEAR,
min: 0,
max: 10,
buckets: 10,
},
index);
this.dispatchEvent(new Event('usage', {bubbles: true, composed: true}));
}
private onDisableButtonClick_() {
const disableEvent = new CustomEvent('disable-module', {
composed: true,
detail: {
message: loadTimeData.getStringF(
'disableModuleToastMessage',
loadTimeData.getString('modulesJourneysSentence2')),
},
});
this.dispatchEvent(disableEvent);
}
private onDismissButtonClick_() {
HistoryClustersProxyImpl.getInstance().handler.dismissCluster(
[this.searchResultPage, ...this.cluster.visits]);
this.dispatchEvent(new CustomEvent('dismiss-module', {
bubbles: true,
composed: true,
detail: {
message: loadTimeData.getStringF(
'dismissModuleToastMessage', this.cluster.label),
},
}));
}
private onInfoButtonClick_() {
this.$.infoDialogRender.get().showModal();
}
private onShowAllClick_() {
assert(this.cluster.label.length >= 2);
HistoryClustersProxyImpl.getInstance().handler.showJourneysSidePanel(
this.cluster.label.substring(1, this.cluster.label.length - 1));
this.recordClick_(HistoryClusterElementType.SHOW_ALL);
}
private onOpenAllInTabGroupClick_() {
const urls = [this.searchResultPage, ...this.cluster.visits].map(
visit => visit.normalizedUrl);
HistoryClustersProxyImpl.getInstance().handler.openUrlsInTabGroup(
urls, this.cluster.tabGroupName ?? null);
}
private shouldShowCartTile_(cart: Object): boolean {
return loadTimeData.getBoolean(
'modulesChromeCartInHistoryClustersModuleEnabled') &&
!!cart;
}
}
customElements.define(
HistoryClustersModuleElement.is, HistoryClustersModuleElement);
function recordSelectedLayout(option: HistoryClusterLayoutType) {
chrome.metricsPrivate.recordEnumerationValue(
'NewTabPage.HistoryClusters.DisplayLayout', option,
Object.keys(HistoryClusterLayoutType).length);
}
function processLayoutVisits(
visits: URLVisit[], numVisits: number, numImageVisits: number): URLVisit[] {
const result: URLVisit[] = Array<URLVisit>(numVisits);
let currentImageIdx = 0;
let currentVisitIdx = numImageVisits;
for (let i = 0; i < visits.length; i++) {
if (currentImageIdx < numImageVisits && visits[i].hasUrlKeyedImage) {
result[currentImageIdx] = visits[i];
currentImageIdx++;
} else if (currentVisitIdx < numVisits) {
result[currentVisitIdx] = visits[i];
currentVisitIdx++;
} else {
break;
}
}
return result;
}
async function createElement(): Promise<HistoryClustersModuleElement|null> {
const {clusters} =
await HistoryClustersProxyImpl.getInstance().handler.getClusters();
// Do not show module if there are no clusters.
if (clusters.length === 0) {
recordSelectedLayout(HistoryClusterLayoutType.NONE);
return null;
}
const element = new HistoryClustersModuleElement();
element.cluster = clusters[0];
// Initialize the cart element when the feature is enabled.
if (loadTimeData.getBoolean(
'modulesChromeCartInHistoryClustersModuleEnabled')) {
const {cart} =
await HistoryClustersProxyImpl.getInstance().handler.getCartForCluster(
clusters[0]);
element.cart = cart;
}
// Pull out the SRP to be used in the header and to open the cluster
// in tab group.
element.searchResultPage = clusters[0]!.visits[0];
// History cluster visits minus the SRP that is included, since the SRP
// isn't used in the layout.
const visits = element.cluster.visits.slice(1);
// Count number of visits with images.
const imageCount = visits
.filter(
(visit: URLVisit) =>
visit.hasUrlKeyedImage && visit.isKnownToSync)
.length;
const visitCount = visits.length;
// Calculate which layout to use.
if (imageCount >= LAYOUT_3_MIN_IMAGE_VISITS) {
// Layout 1 and 3 require the same number of images.
// Decide which one to use by checking if there are enough total
// visits for layout 3.
if (visitCount >= LAYOUT_3_MIN_VISITS) {
element.layoutType = HistoryClusterLayoutType.LAYOUT_3;
element.cluster.visits = processLayoutVisits(
visits, LAYOUT_3_MIN_VISITS, LAYOUT_3_MIN_IMAGE_VISITS);
} else {
// If we have enough image visits, we have enough total visits
// for layout 1, since all visits shown are image visits.
element.layoutType = HistoryClusterLayoutType.LAYOUT_1;
element.cluster.visits = processLayoutVisits(
visits, LAYOUT_1_MIN_VISITS, LAYOUT_1_MIN_IMAGE_VISITS);
}
} else if (
imageCount === LAYOUT_2_MIN_IMAGE_VISITS &&
visitCount >= LAYOUT_2_MIN_VISITS) {
element.layoutType = HistoryClusterLayoutType.LAYOUT_2;
element.cluster.visits = processLayoutVisits(
visits, LAYOUT_2_MIN_VISITS, LAYOUT_2_MIN_IMAGE_VISITS);
} else {
// If the data doesn't fit any layout, don't show the module.
recordSelectedLayout(HistoryClusterLayoutType.NONE);
return null;
}
recordSelectedLayout(element.layoutType);
return element;
}
export const historyClustersDescriptor: ModuleDescriptor = new ModuleDescriptor(
/*id=*/ 'history_clusters', createElement);