TopSites.jsx (6837B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this file, 3 * You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { actionCreators as ac, actionTypes as at } from "common/Actions.mjs"; 6 import { MIN_RICH_FAVICON_SIZE, TOP_SITES_SOURCE } from "./TopSitesConstants"; 7 import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; 8 import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; 9 import { connect } from "react-redux"; 10 import { ModalOverlayWrapper } from "content-src/components/ModalOverlay/ModalOverlay"; 11 import React from "react"; 12 import { SearchShortcutsForm } from "./SearchShortcutsForm"; 13 import { TOP_SITES_MAX_SITES_PER_ROW } from "common/Reducers.sys.mjs"; 14 import { TopSiteForm } from "./TopSiteForm"; 15 import { TopSiteList } from "./TopSite"; 16 17 function topSiteIconType(link) { 18 if (link.customScreenshotURL) { 19 return "custom_screenshot"; 20 } 21 if (link.tippyTopIcon || link.faviconRef === "tippytop") { 22 return "tippytop"; 23 } 24 if (link.faviconSize >= MIN_RICH_FAVICON_SIZE) { 25 return "rich_icon"; 26 } 27 if (link.screenshot) { 28 return "screenshot"; 29 } 30 return "no_image"; 31 } 32 33 /** 34 * Iterates through TopSites and counts types of images. 35 * 36 * @param acc Accumulator for reducer. 37 * @param topsite Entry in TopSites. 38 */ 39 function countTopSitesIconsTypes(topSites) { 40 const countTopSitesTypes = (acc, link) => { 41 acc[topSiteIconType(link)]++; 42 return acc; 43 }; 44 45 return topSites.reduce(countTopSitesTypes, { 46 custom_screenshot: 0, 47 screenshot: 0, 48 tippytop: 0, 49 rich_icon: 0, 50 no_image: 0, 51 }); 52 } 53 54 export class _TopSites extends React.PureComponent { 55 constructor(props) { 56 super(props); 57 this.onEditFormClose = this.onEditFormClose.bind(this); 58 this.onSearchShortcutsFormClose = 59 this.onSearchShortcutsFormClose.bind(this); 60 } 61 62 /** 63 * Dispatch session statistics about the quality of TopSites icons and pinned count. 64 */ 65 _dispatchTopSitesStats() { 66 const topSites = this._getVisibleTopSites().filter( 67 topSite => topSite !== null && topSite !== undefined 68 ); 69 const topSitesIconsStats = countTopSitesIconsTypes(topSites); 70 const topSitesPinned = topSites.filter(site => !!site.isPinned).length; 71 const searchShortcuts = topSites.filter( 72 site => !!site.searchTopSite 73 ).length; 74 // Dispatch telemetry event with the count of TopSites images types. 75 this.props.dispatch( 76 ac.AlsoToMain({ 77 type: at.SAVE_SESSION_PERF_DATA, 78 data: { 79 topsites_icon_stats: topSitesIconsStats, 80 topsites_pinned: topSitesPinned, 81 topsites_search_shortcuts: searchShortcuts, 82 }, 83 }) 84 ); 85 } 86 87 /** 88 * Return the TopSites that are visible based on prefs and window width. 89 */ 90 _getVisibleTopSites() { 91 // We hide 2 sites per row when not in the wide layout. 92 let sitesPerRow = TOP_SITES_MAX_SITES_PER_ROW; 93 // $break-point-widest = 1072px (from _variables.scss) 94 if (!globalThis.matchMedia(`(min-width: 1072px)`).matches) { 95 sitesPerRow -= 2; 96 } 97 return this.props.TopSites.rows.slice( 98 0, 99 this.props.TopSitesRows * sitesPerRow 100 ); 101 } 102 103 componentDidUpdate() { 104 this._dispatchTopSitesStats(); 105 } 106 107 componentDidMount() { 108 this._dispatchTopSitesStats(); 109 } 110 111 onEditFormClose() { 112 this.props.dispatch( 113 ac.UserEvent({ 114 source: TOP_SITES_SOURCE, 115 event: "TOP_SITES_EDIT_CLOSE", 116 }) 117 ); 118 this.props.dispatch({ type: at.TOP_SITES_CANCEL_EDIT }); 119 } 120 121 onSearchShortcutsFormClose() { 122 this.props.dispatch( 123 ac.UserEvent({ 124 source: TOP_SITES_SOURCE, 125 event: "SEARCH_EDIT_CLOSE", 126 }) 127 ); 128 this.props.dispatch({ type: at.TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL }); 129 } 130 131 render() { 132 const { props } = this; 133 const { editForm, showSearchShortcutsForm } = props.TopSites; 134 const extraMenuOptions = ["AddTopSite"]; 135 let visibleTopSites; 136 const colors = props.Prefs.values["newNewtabExperience.colors"]; 137 138 // do not run this function when for startup cache 139 if (!props.App.isForStartupCache.TopSites) { 140 visibleTopSites = this._getVisibleTopSites()?.length; 141 } 142 143 if (props.Prefs.values["improvesearch.topSiteSearchShortcuts"]) { 144 extraMenuOptions.push("AddSearchShortcut"); 145 } 146 147 return ( 148 <ComponentPerfTimer 149 id="topsites" 150 initialized={props.TopSites.initialized} 151 dispatch={props.dispatch} 152 > 153 <CollapsibleSection 154 className="top-sites" 155 id="topsites" 156 title={props.title || { id: "newtab-section-header-topsites" }} 157 hideTitle={true} 158 extraMenuOptions={extraMenuOptions} 159 showPrefName="feeds.topsites" 160 eventSource={TOP_SITES_SOURCE} 161 collapsed={false} 162 isFixed={props.isFixed} 163 isFirst={props.isFirst} 164 isLast={props.isLast} 165 dispatch={props.dispatch} 166 > 167 <TopSiteList 168 TopSites={props.TopSites} 169 TopSitesRows={props.TopSitesRows} 170 dispatch={props.dispatch} 171 topSiteIconType={topSiteIconType} 172 colors={colors} 173 visibleTopSites={visibleTopSites} 174 /> 175 <div className="edit-topsites-wrapper"> 176 {editForm && ( 177 <div className="edit-topsites"> 178 <ModalOverlayWrapper 179 unstyled={true} 180 onClose={this.onEditFormClose} 181 innerClassName="modal" 182 > 183 <TopSiteForm 184 site={props.TopSites.rows[editForm.index]} 185 onClose={this.onEditFormClose} 186 dispatch={this.props.dispatch} 187 {...editForm} 188 /> 189 </ModalOverlayWrapper> 190 </div> 191 )} 192 {showSearchShortcutsForm && ( 193 <div className="edit-search-shortcuts"> 194 <ModalOverlayWrapper 195 unstyled={true} 196 onClose={this.onSearchShortcutsFormClose} 197 innerClassName="modal" 198 > 199 <SearchShortcutsForm 200 TopSites={props.TopSites} 201 onClose={this.onSearchShortcutsFormClose} 202 dispatch={this.props.dispatch} 203 /> 204 </ModalOverlayWrapper> 205 </div> 206 )} 207 </div> 208 </CollapsibleSection> 209 </ComponentPerfTimer> 210 ); 211 } 212 } 213 214 export const TopSites = connect(state => ({ 215 App: state.App, 216 TopSites: state.TopSites, 217 Prefs: state.Prefs, 218 TopSitesRows: state.Prefs.values.topSitesRows, 219 }))(_TopSites);