/* 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 && (