- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 88 %
- : 77 %
- : 77 %
- : 77 %
- : 77 %
- : 77 %
- : 77 %
- : 77 %
Source code
Revision control
Copy as Markdown
Other Tools
/* 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 { actionCreators as ac, actionTypes as at } from "common/Actions.mjs";
import { MIN_RICH_FAVICON_SIZE, TOP_SITES_SOURCE } from "./TopSitesConstants";
import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer";
import { ErrorBoundary } from "content-src/components/ErrorBoundary/ErrorBoundary";
import { connect } from "react-redux";
import { ModalOverlayWrapper } from "content-src/components/ModalOverlay/ModalOverlay";
import React from "react";
import { SearchShortcutsForm } from "./SearchShortcutsForm";
import { TOP_SITES_MAX_SITES_PER_ROW } from "common/Reducers.sys.mjs";
import { TopSiteForm } from "./TopSiteForm";
import { TopSiteList } from "./TopSite";
// @nova-cleanup(remove-pref): Remove once classic path is gone
const PREF_NOVA_ENABLED = "nova.enabled";
function topSiteIconType(link) {
if (link.customScreenshotURL) {
return "custom_screenshot";
}
if (link.tippyTopIcon || link.faviconRef === "tippytop") {
return "tippytop";
}
if (link.faviconSize >= MIN_RICH_FAVICON_SIZE) {
return "rich_icon";
}
if (link.screenshot) {
return "screenshot";
}
return "no_image";
}
/**
* Iterates through TopSites and counts types of images.
*
* @param acc Accumulator for reducer.
* @param topsite Entry in TopSites.
*/
function countTopSitesIconsTypes(topSites) {
const countTopSitesTypes = (acc, link) => {
acc[topSiteIconType(link)]++;
return acc;
};
return topSites.reduce(countTopSitesTypes, {
custom_screenshot: 0,
screenshot: 0,
tippytop: 0,
rich_icon: 0,
no_image: 0,
});
}
function getTopSiteGridCols(fallback) {
const grid = globalThis.document?.querySelector(".top-sites-list");
if (!grid) {
return fallback;
}
return globalThis.getComputedStyle(grid).gridTemplateColumns.split(" ")
.length;
}
export class _TopSites extends React.PureComponent {
constructor(props) {
super(props);
this.onEditFormClose = this.onEditFormClose.bind(this);
this.onSearchShortcutsFormClose =
this.onSearchShortcutsFormClose.bind(this);
}
/**
* Dispatch session statistics about the quality of TopSites icons and pinned count.
*/
_dispatchTopSitesStats() {
const topSites = this._getVisibleTopSites().filter(
topSite => topSite !== null && topSite !== undefined
);
const topSitesIconsStats = countTopSitesIconsTypes(topSites);
const topSitesPinned = topSites.filter(site => !!site.isPinned).length;
const searchShortcuts = topSites.filter(
site => !!site.searchTopSite
).length;
// Dispatch telemetry event with the count of TopSites images types.
this.props.dispatch(
ac.AlsoToMain({
type: at.SAVE_SESSION_PERF_DATA,
data: {
topsites_icon_stats: topSitesIconsStats,
topsites_pinned: topSitesPinned,
topsites_search_shortcuts: searchShortcuts,
},
})
);
}
/**
* Return the TopSites that are visible based on prefs and window width.
*/
_getVisibleTopSites() {
const novaEnabled = this.props.Prefs.values[PREF_NOVA_ENABLED];
let sitesPerRow = this.props.TopSitesMaxSitesPerRow;
if (novaEnabled) {
sitesPerRow = getTopSiteGridCols(sitesPerRow);
} else if (!globalThis.matchMedia("(min-width: 1072px)").matches) {
sitesPerRow = 6;
} else if (
sitesPerRow > 8 &&
!globalThis.matchMedia("(min-width: 1374px)").matches
) {
sitesPerRow = 8;
}
return this.props.TopSites.rows.slice(
0,
this.props.TopSitesRows * sitesPerRow
);
}
componentDidUpdate() {
this._dispatchTopSitesStats();
}
componentDidMount() {
this._dispatchTopSitesStats();
}
onEditFormClose() {
this.props.dispatch(
ac.UserEvent({
source: TOP_SITES_SOURCE,
event: "TOP_SITES_EDIT_CLOSE",
})
);
this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT });
}
onSearchShortcutsFormClose() {
this.props.dispatch(
ac.UserEvent({
source: TOP_SITES_SOURCE,
event: "SEARCH_EDIT_CLOSE",
})
);
this.props.dispatch({ type: at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL });
}
render() {
const { props } = this;
const { editForm, showSearchShortcutsForm } = props.TopSites;
let visibleTopSites;
const colors = props.Prefs.values["newNewtabExperience.colors"];
// do not run this function when for startup cache
if (!props.App.isForStartupCache.TopSites) {
visibleTopSites = this._getVisibleTopSites()?.length;
}
return (
<ComponentPerfTimer
id="topsites"
initialized={props.TopSites.initialized}
dispatch={props.dispatch}
>
<section className="top-sites" data-section-id="topsites">
{/* nova-cleanup: Can be removed once classic path is gone since Nova wraps TopSites in ErrorBoundary in Base.jsx */}
<ErrorBoundary className="section-body-fallback">
<TopSiteList
TopSites={props.TopSites}
TopSitesRows={props.TopSitesRows}
topSitesMaxSitesPerRow={props.TopSitesMaxSitesPerRow}
dispatch={props.dispatch}
topSiteIconType={topSiteIconType}
colors={colors}
visibleTopSites={visibleTopSites}
/>
<div className="edit-topsites-wrapper">
{editForm && (
<div className="edit-topsites">
<ModalOverlayWrapper
unstyled={true}
onClose={this.onEditFormClose}
innerClassName="modal"
>
<TopSiteForm
site={props.TopSites.rows[editForm.index]}
onClose={this.onEditFormClose}
dispatch={this.props.dispatch}
{...editForm}
/>
</ModalOverlayWrapper>
</div>
)}
{showSearchShortcutsForm && (
<div className="edit-search-shortcuts">
<ModalOverlayWrapper
unstyled={true}
onClose={this.onSearchShortcutsFormClose}
innerClassName="modal"
>
<SearchShortcutsForm
TopSites={props.TopSites}
onClose={this.onSearchShortcutsFormClose}
dispatch={this.props.dispatch}
/>
</ModalOverlayWrapper>
</div>
)}
</div>
</ErrorBoundary>
</section>
</ComponentPerfTimer>
);
}
}
export const TopSites = connect(state => {
const prefs = state.Prefs.values;
return {
App: state.App,
TopSites: state.TopSites,
Prefs: state.Prefs,
TopSitesRows: prefs.topSitesRows,
TopSitesMaxSitesPerRow:
prefs.trainhopConfig?.topSites?.maxSitesPerRow ??
prefs.topSitesMaxSitesPerRow ??
TOP_SITES_MAX_SITES_PER_ROW,
};
})(_TopSites);