From 6bf0a5cb5034a7e684dcc3500e841785237ce2dd Mon Sep 17 00:00:00 2001 From: Daniel Baumann Date: Sun, 7 Apr 2024 19:32:43 +0200 Subject: Adding upstream version 1:115.7.0. Signed-off-by: Daniel Baumann --- .../content-src/components/Sections/Sections.jsx | 378 +++++++++++++++++++++ 1 file changed, 378 insertions(+) create mode 100644 browser/components/newtab/content-src/components/Sections/Sections.jsx (limited to 'browser/components/newtab/content-src/components/Sections/Sections.jsx') diff --git a/browser/components/newtab/content-src/components/Sections/Sections.jsx b/browser/components/newtab/content-src/components/Sections/Sections.jsx new file mode 100644 index 0000000000..e72e9145ad --- /dev/null +++ b/browser/components/newtab/content-src/components/Sections/Sections.jsx @@ -0,0 +1,378 @@ +/* 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.sys.mjs"; +import { Card, PlaceholderCard } from "content-src/components/Card/Card"; +import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection"; +import { ComponentPerfTimer } from "content-src/components/ComponentPerfTimer/ComponentPerfTimer"; +import { FluentOrText } from "content-src/components/FluentOrText/FluentOrText"; +import { connect } from "react-redux"; +import { MoreRecommendations } from "content-src/components/MoreRecommendations/MoreRecommendations"; +import { PocketLoggedInCta } from "content-src/components/PocketLoggedInCta/PocketLoggedInCta"; +import React from "react"; +import { Topics } from "content-src/components/Topics/Topics"; +import { TopSites } from "content-src/components/TopSites/TopSites"; + +const VISIBLE = "visible"; +const VISIBILITY_CHANGE_EVENT = "visibilitychange"; +const CARDS_PER_ROW_DEFAULT = 3; +const CARDS_PER_ROW_COMPACT_WIDE = 4; + +export class Section extends React.PureComponent { + get numRows() { + const { rowsPref, maxRows, Prefs } = this.props; + return rowsPref ? Prefs.values[rowsPref] : maxRows; + } + + _dispatchImpressionStats() { + const { props } = this; + let cardsPerRow = CARDS_PER_ROW_DEFAULT; + if ( + props.compactCards && + global.matchMedia(`(min-width: 1072px)`).matches + ) { + // If the section has compact cards and the viewport is wide enough, we show + // 4 columns instead of 3. + // $break-point-widest = 1072px (from _variables.scss) + cardsPerRow = CARDS_PER_ROW_COMPACT_WIDE; + } + const maxCards = cardsPerRow * this.numRows; + const cards = props.rows.slice(0, maxCards); + + if (this.needsImpressionStats(cards)) { + props.dispatch( + ac.ImpressionStats({ + source: props.eventSource, + tiles: cards.map(link => ({ id: link.guid })), + }) + ); + this.impressionCardGuids = cards.map(link => link.guid); + } + } + + // This sends an event when a user sees a set of new content. If content + // changes while the page is hidden (i.e. preloaded or on a hidden tab), + // only send the event if the page becomes visible again. + sendImpressionStatsOrAddListener() { + const { props } = this; + + if (!props.shouldSendImpressionStats || !props.dispatch) { + return; + } + + if (props.document.visibilityState === VISIBLE) { + this._dispatchImpressionStats(); + } else { + // We should only ever send the latest impression stats ping, so remove any + // older listeners. + if (this._onVisibilityChange) { + props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + + // When the page becomes visible, send the impression stats ping if the section isn't collapsed. + this._onVisibilityChange = () => { + if (props.document.visibilityState === VISIBLE) { + if (!this.props.pref.collapsed) { + this._dispatchImpressionStats(); + } + props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + }; + props.document.addEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + componentWillMount() { + this.sendNewTabRehydrated(this.props.initialized); + } + + componentDidMount() { + if (this.props.rows.length && !this.props.pref.collapsed) { + this.sendImpressionStatsOrAddListener(); + } + } + + componentDidUpdate(prevProps) { + const { props } = this; + const isCollapsed = props.pref.collapsed; + const wasCollapsed = prevProps.pref.collapsed; + if ( + // Don't send impression stats for the empty state + props.rows.length && + // We only want to send impression stats if the content of the cards has changed + // and the section is not collapsed... + ((props.rows !== prevProps.rows && !isCollapsed) || + // or if we are expanding a section that was collapsed. + (wasCollapsed && !isCollapsed)) + ) { + this.sendImpressionStatsOrAddListener(); + } + } + + componentWillUpdate(nextProps) { + this.sendNewTabRehydrated(nextProps.initialized); + } + + componentWillUnmount() { + if (this._onVisibilityChange) { + this.props.document.removeEventListener( + VISIBILITY_CHANGE_EVENT, + this._onVisibilityChange + ); + } + } + + needsImpressionStats(cards) { + if ( + !this.impressionCardGuids || + this.impressionCardGuids.length !== cards.length + ) { + return true; + } + + for (let i = 0; i < cards.length; i++) { + if (cards[i].guid !== this.impressionCardGuids[i]) { + return true; + } + } + + return false; + } + + // The NEW_TAB_REHYDRATED event is used to inform feeds that their + // data has been consumed e.g. for counting the number of tabs that + // have rendered that data. + sendNewTabRehydrated(initialized) { + if (initialized && !this.renderNotified) { + this.props.dispatch( + ac.AlsoToMain({ type: at.NEW_TAB_REHYDRATED, data: {} }) + ); + this.renderNotified = true; + } + } + + render() { + const { + id, + eventSource, + title, + rows, + Pocket, + topics, + emptyState, + dispatch, + compactCards, + read_more_endpoint, + contextMenuOptions, + initialized, + learnMore, + pref, + privacyNoticeURL, + isFirst, + isLast, + } = this.props; + + const waitingForSpoc = + id === "topstories" && this.props.Pocket.waitingForSpoc; + const maxCardsPerRow = compactCards + ? CARDS_PER_ROW_COMPACT_WIDE + : CARDS_PER_ROW_DEFAULT; + const { numRows } = this; + const maxCards = maxCardsPerRow * numRows; + const maxCardsOnNarrow = CARDS_PER_ROW_DEFAULT * numRows; + + const { pocketCta, isUserLoggedIn } = Pocket || {}; + const { useCta } = pocketCta || {}; + + // Don't display anything until we have a definitve result from Pocket, + // to avoid a flash of logged out state while we render. + const isPocketLoggedInDefined = + isUserLoggedIn === true || isUserLoggedIn === false; + + const hasTopics = topics && !!topics.length; + + const shouldShowPocketCta = + id === "topstories" && useCta && isUserLoggedIn === false; + + // Show topics only for top stories and if it has loaded with topics. + // The classs .top-stories-bottom-container ensures content doesn't shift as things load. + const shouldShowTopics = + id === "topstories" && + hasTopics && + ((useCta && isUserLoggedIn === true) || + (!useCta && isPocketLoggedInDefined)); + + // We use topics to determine language support for read more. + const shouldShowReadMore = read_more_endpoint && hasTopics; + + const realRows = rows.slice(0, maxCards); + + // The empty state should only be shown after we have initialized and there is no content. + // Otherwise, we should show placeholders. + const shouldShowEmptyState = initialized && !rows.length; + + const cards = []; + if (!shouldShowEmptyState) { + for (let i = 0; i < maxCards; i++) { + const link = realRows[i]; + // On narrow viewports, we only show 3 cards per row. We'll mark the rest as + // .hide-for-narrow to hide in CSS via @media query. + const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : ""; + let usePlaceholder = !link; + // If we are in the third card and waiting for spoc, + // use the placeholder. + if (!usePlaceholder && i === 2 && waitingForSpoc) { + usePlaceholder = true; + } + cards.push( + !usePlaceholder ? ( + + ) : ( + + ) + ); + } + } + + const sectionClassName = [ + "section", + compactCards ? "compact-cards" : "normal-cards", + ].join(" "); + + //
<-- React component + //
<-- HTML5 element + return ( + + + {!shouldShowEmptyState && ( +
    + {cards} +
+ )} + {shouldShowEmptyState && ( +
+
+ +

+ +

+
+ )} + {id === "topstories" && ( +
+ {shouldShowTopics && ( +
+ +
+ )} + + {shouldShowPocketCta && ( +
+ +
+ )} + +
+ {shouldShowReadMore && ( + + )} +
+
+ )} +
+
+ ); + } +} + +Section.defaultProps = { + document: global.document, + rows: [], + emptyState: {}, + pref: {}, + title: "", +}; + +export const SectionIntl = connect(state => ({ + Prefs: state.Prefs, + Pocket: state.Pocket, +}))(Section); + +export class _Sections extends React.PureComponent { + renderSections() { + const sections = []; + const enabledSections = this.props.Sections.filter( + section => section.enabled + ); + const { sectionOrder, "feeds.topsites": showTopSites } = + this.props.Prefs.values; + // Enabled sections doesn't include Top Sites, so we add it if enabled. + const expectedCount = enabledSections.length + ~~showTopSites; + + for (const sectionId of sectionOrder.split(",")) { + const commonProps = { + key: sectionId, + isFirst: sections.length === 0, + isLast: sections.length === expectedCount - 1, + }; + if (sectionId === "topsites" && showTopSites) { + sections.push(); + } else { + const section = enabledSections.find(s => s.id === sectionId); + if (section) { + sections.push(); + } + } + } + return sections; + } + + render() { + return
{this.renderSections()}
; + } +} + +export const Sections = connect(state => ({ + Sections: state.Sections, + Prefs: state.Prefs, +}))(_Sections); -- cgit v1.2.3