/* 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);