зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1774473 - Pocket newtab recent saves section. r=gvn
Differential Revision: https://phabricator.services.mozilla.com/D150196
This commit is contained in:
Родитель
406fcd5384
Коммит
301b474fd4
|
@ -1537,6 +1537,7 @@ pref("browser.newtabpage.activity-stream.discoverystream.descLines", 3);
|
|||
pref("browser.newtabpage.activity-stream.discoverystream.readTime.enabled", true);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.newSponsoredLabel.enabled", false);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.essentialReadsHeader.enabled", false);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.recentSaves.enabled", false);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.editorsPicksHeader.enabled", false);
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.spoc-positions", "1,5,7,11,18,20");
|
||||
pref("browser.newtabpage.activity-stream.discoverystream.widget-positions", "");
|
||||
|
|
|
@ -63,6 +63,10 @@ for (const type of [
|
|||
"DISCOVERY_STREAM_PERSONALIZATION_INIT",
|
||||
"DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED",
|
||||
"DISCOVERY_STREAM_PERSONALIZATION_TOGGLE",
|
||||
"DISCOVERY_STREAM_POCKET_STATE_INIT",
|
||||
"DISCOVERY_STREAM_POCKET_STATE_SET",
|
||||
"DISCOVERY_STREAM_PREFS_SETUP",
|
||||
"DISCOVERY_STREAM_RECENT_SAVES",
|
||||
"DISCOVERY_STREAM_RETRY_FEED",
|
||||
"DISCOVERY_STREAM_SPOCS_CAPS",
|
||||
"DISCOVERY_STREAM_SPOCS_ENDPOINT",
|
||||
|
|
|
@ -81,6 +81,9 @@ const INITIAL_STATE = {
|
|||
utmCampaign: undefined,
|
||||
utmContent: undefined,
|
||||
},
|
||||
recentSavesData: [],
|
||||
isUserLoggedIn: false,
|
||||
recentSavesEnabled: false,
|
||||
},
|
||||
Personalization: {
|
||||
lastUpdated: null,
|
||||
|
@ -639,6 +642,21 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
|
|||
...prevState,
|
||||
isCollectionDismissible: action.data.value,
|
||||
};
|
||||
case at.DISCOVERY_STREAM_PREFS_SETUP:
|
||||
return {
|
||||
...prevState,
|
||||
recentSavesEnabled: action.data.recentSavesEnabled,
|
||||
};
|
||||
case at.DISCOVERY_STREAM_RECENT_SAVES:
|
||||
return {
|
||||
...prevState,
|
||||
recentSavesData: action.data.recentSaves,
|
||||
};
|
||||
case at.DISCOVERY_STREAM_POCKET_STATE_SET:
|
||||
return {
|
||||
...prevState,
|
||||
isUserLoggedIn: action.data.isUserLoggedIn,
|
||||
};
|
||||
case at.HIDE_PRIVACY_INFO:
|
||||
return {
|
||||
...prevState,
|
||||
|
|
|
@ -10,8 +10,9 @@ import {
|
|||
import { DSEmptyState } from "../DSEmptyState/DSEmptyState.jsx";
|
||||
import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx";
|
||||
import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx";
|
||||
import { actionCreators as ac } from "common/Actions.jsm";
|
||||
import React from "react";
|
||||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||
import React, { useEffect, useState, useRef, useCallback } from "react";
|
||||
import { connect, useSelector } from "react-redux";
|
||||
const WIDGET_IDS = {
|
||||
TOPICS: 1,
|
||||
};
|
||||
|
@ -40,7 +41,133 @@ export function GridContainer(props) {
|
|||
);
|
||||
}
|
||||
|
||||
export class CardGrid extends React.PureComponent {
|
||||
export function IntersectionObserver({
|
||||
children,
|
||||
windowObj = window,
|
||||
onIntersecting,
|
||||
}) {
|
||||
const intersectionElement = useRef(null);
|
||||
|
||||
useEffect(() => {
|
||||
let observer;
|
||||
if (!observer && onIntersecting && intersectionElement.current) {
|
||||
observer = new windowObj.IntersectionObserver(entries => {
|
||||
const entry = entries.find(e => e.isIntersecting);
|
||||
|
||||
if (entry) {
|
||||
// Stop observing since element has been seen
|
||||
if (observer && intersectionElement.current) {
|
||||
observer.unobserve(intersectionElement.current);
|
||||
}
|
||||
|
||||
onIntersecting();
|
||||
}
|
||||
});
|
||||
observer.observe(intersectionElement.current);
|
||||
}
|
||||
// Cleanup
|
||||
return () => observer?.disconnect();
|
||||
}, [windowObj, onIntersecting]);
|
||||
|
||||
return <div ref={intersectionElement}>{children}</div>;
|
||||
}
|
||||
|
||||
export function RecentSavesContainer({
|
||||
className,
|
||||
dispatch,
|
||||
windowObj = window,
|
||||
items = 3,
|
||||
}) {
|
||||
const { recentSavesData, isUserLoggedIn } = useSelector(
|
||||
state => state.DiscoveryStream
|
||||
);
|
||||
|
||||
const [visible, setVisible] = useState(false);
|
||||
const onIntersecting = useCallback(() => setVisible(true), []);
|
||||
|
||||
useEffect(() => {
|
||||
if (visible) {
|
||||
dispatch(
|
||||
ac.AlsoToMain({
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
|
||||
})
|
||||
);
|
||||
}
|
||||
}, [visible, dispatch]);
|
||||
|
||||
// The user has not yet scrolled to this section,
|
||||
// so wait before potentially requesting Pocket data.
|
||||
if (!visible) {
|
||||
return (
|
||||
<IntersectionObserver
|
||||
windowObj={windowObj}
|
||||
onIntersecting={onIntersecting}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
// Intersection observer has finished, but we're not yet logged in.
|
||||
if (visible && !isUserLoggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderCard(rec, index) {
|
||||
return (
|
||||
<DSCard
|
||||
key={`dscard-${rec?.id || index}`}
|
||||
id={rec.id}
|
||||
pos={index}
|
||||
type="CARDGRID_RECENT_SAVES"
|
||||
image_src={rec.image_src}
|
||||
raw_image_src={rec.raw_image_src}
|
||||
word_count={rec.word_count}
|
||||
time_to_read={rec.time_to_read}
|
||||
title={rec.title}
|
||||
excerpt={rec.excerpt}
|
||||
url={rec.url}
|
||||
source={rec.domain}
|
||||
isRecentSave={true}
|
||||
dispatch={dispatch}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
const recentSavesCards = [];
|
||||
// We fill the cards with a for loop over an inline map because
|
||||
// we want empty placeholders if there are not enough cards.
|
||||
for (let index = 0; index < items; index++) {
|
||||
const recentSave = recentSavesData[index];
|
||||
if (!recentSave) {
|
||||
recentSavesCards.push(<PlaceholderDSCard key={`dscard-${index}`} />);
|
||||
} else {
|
||||
recentSavesCards.push(
|
||||
renderCard(
|
||||
{
|
||||
id: recentSave.item_id || recentSave.resolved_id,
|
||||
image_src: recentSave.top_image_url,
|
||||
raw_image_src: recentSave.top_image_url,
|
||||
word_count: recentSave.word_count,
|
||||
time_to_read: recentSave.time_to_read,
|
||||
title: recentSave.resolved_title,
|
||||
url: recentSave.resolved_url,
|
||||
domain: recentSave.domain_metadata?.name,
|
||||
excerpt: recentSave.excerpt,
|
||||
},
|
||||
index
|
||||
)
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// We are visible and logged in.
|
||||
return (
|
||||
<GridContainer className={className} header="Recently Saved to your List">
|
||||
{recentSavesCards}
|
||||
</GridContainer>
|
||||
);
|
||||
}
|
||||
|
||||
export class _CardGrid extends React.PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = { moreLoaded: false };
|
||||
|
@ -68,6 +195,8 @@ export class CardGrid extends React.PureComponent {
|
|||
|
||||
renderCards() {
|
||||
let { items } = this.props;
|
||||
const { DiscoveryStream } = this.props;
|
||||
const { recentSavesEnabled } = DiscoveryStream;
|
||||
const {
|
||||
hybridLayout,
|
||||
hideCardBackground,
|
||||
|
@ -191,18 +320,23 @@ export class CardGrid extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
let moreRecsHeader = "";
|
||||
// For now this is English only.
|
||||
if (essentialReadsHeader && editorsPicksHeader) {
|
||||
if (recentSavesEnabled || (essentialReadsHeader && editorsPicksHeader)) {
|
||||
let spliceAt = 6;
|
||||
// For 4 card row layouts, second row is 8 cards, and regular it is 6 cards.
|
||||
if (fourCardLayout) {
|
||||
spliceAt = 8;
|
||||
}
|
||||
// If we have a custom header, ensure the more recs section also has a header.
|
||||
moreRecsHeader = "More Recommendations";
|
||||
// Put the first 2 rows into essentialReadsCards.
|
||||
essentialReadsCards = [...cards.splice(0, spliceAt)];
|
||||
// Put the rest into editorsPicksCards.
|
||||
if (essentialReadsHeader && editorsPicksHeader) {
|
||||
editorsPicksCards = [...cards.splice(0, cards.length)];
|
||||
}
|
||||
}
|
||||
|
||||
// Used for CSS overrides to default styling (eg: "hero")
|
||||
const variantClass = this.props.display_variant
|
||||
|
@ -231,13 +365,21 @@ export class CardGrid extends React.PureComponent {
|
|||
{essentialReadsCards}
|
||||
</GridContainer>
|
||||
)}
|
||||
{recentSavesEnabled && (
|
||||
<RecentSavesContainer
|
||||
className={className}
|
||||
dispatch={this.props.dispatch}
|
||||
/>
|
||||
)}
|
||||
{editorsPicksCards?.length > 0 && (
|
||||
<GridContainer className={className} header="Editor’s Picks">
|
||||
{editorsPicksCards}
|
||||
</GridContainer>
|
||||
)}
|
||||
{cards?.length > 0 && (
|
||||
<GridContainer className={className}>{cards}</GridContainer>
|
||||
<GridContainer className={className} header={moreRecsHeader}>
|
||||
{cards}
|
||||
</GridContainer>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
|
@ -289,7 +431,7 @@ export class CardGrid extends React.PureComponent {
|
|||
}
|
||||
}
|
||||
|
||||
CardGrid.defaultProps = {
|
||||
_CardGrid.defaultProps = {
|
||||
border: `border`,
|
||||
items: 4, // Number of stories to display
|
||||
enable_video_playheads: false,
|
||||
|
@ -297,3 +439,7 @@ CardGrid.defaultProps = {
|
|||
saveToPocketCard: false,
|
||||
loadMoreThreshold: 12,
|
||||
};
|
||||
|
||||
export const CardGrid = connect(state => ({
|
||||
DiscoveryStream: state.DiscoveryStream,
|
||||
}))(_CardGrid);
|
||||
|
|
|
@ -85,6 +85,7 @@ export const DefaultMeta = ({
|
|||
sponsor,
|
||||
sponsored_by_override,
|
||||
saveToPocketCard,
|
||||
isRecentSave,
|
||||
}) => (
|
||||
<div className="meta">
|
||||
<div className="info-wrap">
|
||||
|
@ -418,6 +419,7 @@ export class _DSCard extends React.PureComponent {
|
|||
titleLines = 3,
|
||||
descLines = 3,
|
||||
displayReadTime,
|
||||
isRecentSave,
|
||||
} = this.props;
|
||||
const excerpt = !hideDescriptions ? this.props.excerpt : "";
|
||||
|
||||
|
@ -541,6 +543,7 @@ export class _DSCard extends React.PureComponent {
|
|||
onMenuShow={this.onMenuShow}
|
||||
saveToPocketCard={saveToPocketCard}
|
||||
pocket_button_enabled={this.props.pocket_button_enabled}
|
||||
isRecentSave={isRecentSave}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
@ -565,6 +568,7 @@ export class _DSCard extends React.PureComponent {
|
|||
onMenuUpdate={this.onMenuUpdate}
|
||||
onMenuShow={this.onMenuShow}
|
||||
pocket_button_enabled={this.props.pocket_button_enabled}
|
||||
isRecentSave={isRecentSave}
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
|
|
|
@ -10,12 +10,17 @@ export class DSLinkMenu extends React.PureComponent {
|
|||
render() {
|
||||
const { index, dispatch } = this.props;
|
||||
let pocketMenuOptions = [];
|
||||
let TOP_STORIES_CONTEXT_MENU_OPTIONS = [
|
||||
"OpenInNewWindow",
|
||||
"OpenInPrivateWindow",
|
||||
];
|
||||
if (!this.props.isRecentSave) {
|
||||
if (this.props.pocket_button_enabled) {
|
||||
pocketMenuOptions = this.props.saveToPocketCard
|
||||
? ["CheckDeleteFromPocket"]
|
||||
: ["CheckSavedToPocket"];
|
||||
}
|
||||
const TOP_STORIES_CONTEXT_MENU_OPTIONS = [
|
||||
TOP_STORIES_CONTEXT_MENU_OPTIONS = [
|
||||
"CheckBookmark",
|
||||
"CheckArchiveFromPocket",
|
||||
...pocketMenuOptions,
|
||||
|
@ -26,6 +31,7 @@ export class DSLinkMenu extends React.PureComponent {
|
|||
"BlockUrl",
|
||||
...(this.props.showPrivacyInfo ? ["ShowPrivacyInfo"] : []),
|
||||
];
|
||||
}
|
||||
const type = this.props.type || "DISCOVERY_STREAM";
|
||||
const title = this.props.title || this.props.source;
|
||||
|
||||
|
|
|
@ -103,7 +103,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
|
|||
|
||||
const actionTypes = {};
|
||||
|
||||
for (const type of ["ABOUT_SPONSORED_TOP_SITES", "ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TARGETING_UPDATE", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "CLEAR_PREF", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_SEARCH", "DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_RESET", "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_DEV_EXPIRE_CACHE", "DISCOVERY_STREAM_DEV_IDLE_DAILY", "DISCOVERY_STREAM_DEV_SYNC_RS", "DISCOVERY_STREAM_DEV_SYSTEM_TICK", "DISCOVERY_STREAM_EXPERIMENT_DATA", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_PERSONALIZATION_INIT", "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", "DISCOVERY_STREAM_PERSONALIZATION_TOGGLE", "DISCOVERY_STREAM_RETRY_FEED", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_PLACEMENTS", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_BLOCKED", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_PRIVACY_INFO", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PARTNER_LINK_ATTRIBUTION", "PLACES_BOOKMARKS_REMOVED", "PLACES_BOOKMARK_ADDED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINKS_DELETED", "PLACES_LINK_BLOCKED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_PRIVACY_INFO", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SUBMIT_SIGNIN", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_IMPRESSION_STATS", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
|
||||
for (const type of ["ABOUT_SPONSORED_TOP_SITES", "ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TARGETING_UPDATE", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "CLEAR_PREF", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_SEARCH", "DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_RESET", "DISCOVERY_STREAM_CONFIG_RESET_DEFAULTS", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_DEV_EXPIRE_CACHE", "DISCOVERY_STREAM_DEV_IDLE_DAILY", "DISCOVERY_STREAM_DEV_SYNC_RS", "DISCOVERY_STREAM_DEV_SYSTEM_TICK", "DISCOVERY_STREAM_EXPERIMENT_DATA", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_PERSONALIZATION_INIT", "DISCOVERY_STREAM_PERSONALIZATION_LAST_UPDATED", "DISCOVERY_STREAM_PERSONALIZATION_TOGGLE", "DISCOVERY_STREAM_POCKET_STATE_INIT", "DISCOVERY_STREAM_POCKET_STATE_SET", "DISCOVERY_STREAM_PREFS_SETUP", "DISCOVERY_STREAM_RECENT_SAVES", "DISCOVERY_STREAM_RETRY_FEED", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_PLACEMENTS", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_BLOCKED", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_PRIVACY_INFO", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PARTNER_LINK_ATTRIBUTION", "PLACES_BOOKMARKS_REMOVED", "PLACES_BOOKMARK_ADDED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINKS_DELETED", "PLACES_LINK_BLOCKED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_PRIVACY_INFO", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SUBMIT_SIGNIN", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_IMPRESSION_STATS", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
|
||||
actionTypes[type] = type;
|
||||
} // Helper function for creating routed actions between content and main
|
||||
// Not intended to be used by consumers
|
||||
|
@ -6864,12 +6864,16 @@ class DSLinkMenu extends (external_React_default()).PureComponent {
|
|||
dispatch
|
||||
} = this.props;
|
||||
let pocketMenuOptions = [];
|
||||
let TOP_STORIES_CONTEXT_MENU_OPTIONS = ["OpenInNewWindow", "OpenInPrivateWindow"];
|
||||
|
||||
if (!this.props.isRecentSave) {
|
||||
if (this.props.pocket_button_enabled) {
|
||||
pocketMenuOptions = this.props.saveToPocketCard ? ["CheckDeleteFromPocket"] : ["CheckSavedToPocket"];
|
||||
}
|
||||
|
||||
const TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", "CheckArchiveFromPocket", ...pocketMenuOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", ...(this.props.showPrivacyInfo ? ["ShowPrivacyInfo"] : [])];
|
||||
TOP_STORIES_CONTEXT_MENU_OPTIONS = ["CheckBookmark", "CheckArchiveFromPocket", ...pocketMenuOptions, "Separator", "OpenInNewWindow", "OpenInPrivateWindow", "Separator", "BlockUrl", ...(this.props.showPrivacyInfo ? ["ShowPrivacyInfo"] : [])];
|
||||
}
|
||||
|
||||
const type = this.props.type || "DISCOVERY_STREAM";
|
||||
const title = this.props.title || this.props.source;
|
||||
return /*#__PURE__*/external_React_default().createElement("div", {
|
||||
|
@ -7485,7 +7489,8 @@ const DefaultMeta = ({
|
|||
cta_variant,
|
||||
sponsor,
|
||||
sponsored_by_override,
|
||||
saveToPocketCard
|
||||
saveToPocketCard,
|
||||
isRecentSave
|
||||
}) => /*#__PURE__*/external_React_default().createElement("div", {
|
||||
className: "meta"
|
||||
}, /*#__PURE__*/external_React_default().createElement("div", {
|
||||
|
@ -7779,7 +7784,8 @@ class _DSCard extends (external_React_default()).PureComponent {
|
|||
imageGradient,
|
||||
titleLines = 3,
|
||||
descLines = 3,
|
||||
displayReadTime
|
||||
displayReadTime,
|
||||
isRecentSave
|
||||
} = this.props;
|
||||
const excerpt = !hideDescriptions ? this.props.excerpt : "";
|
||||
let timeToRead;
|
||||
|
@ -7879,7 +7885,8 @@ class _DSCard extends (external_React_default()).PureComponent {
|
|||
onMenuUpdate: this.onMenuUpdate,
|
||||
onMenuShow: this.onMenuShow,
|
||||
saveToPocketCard: saveToPocketCard,
|
||||
pocket_button_enabled: this.props.pocket_button_enabled
|
||||
pocket_button_enabled: this.props.pocket_button_enabled,
|
||||
isRecentSave: isRecentSave
|
||||
}))), !saveToPocketCard && /*#__PURE__*/external_React_default().createElement(DSLinkMenu, {
|
||||
id: this.props.id,
|
||||
index: this.props.pos,
|
||||
|
@ -7896,7 +7903,8 @@ class _DSCard extends (external_React_default()).PureComponent {
|
|||
hostRef: this.contextMenuButtonHostRef,
|
||||
onMenuUpdate: this.onMenuUpdate,
|
||||
onMenuShow: this.onMenuShow,
|
||||
pocket_button_enabled: this.props.pocket_button_enabled
|
||||
pocket_button_enabled: this.props.pocket_button_enabled,
|
||||
isRecentSave: isRecentSave
|
||||
}));
|
||||
}
|
||||
|
||||
|
@ -8141,6 +8149,7 @@ const TopicsWidget = (0,external_ReactRedux_namespaceObject.connect)(state => ({
|
|||
|
||||
|
||||
|
||||
|
||||
const WIDGET_IDS = {
|
||||
TOPICS: 1
|
||||
};
|
||||
|
@ -8165,7 +8174,128 @@ function GridContainer(props) {
|
|||
className: `ds-card-grid ${className}`
|
||||
}, children));
|
||||
}
|
||||
class CardGrid extends (external_React_default()).PureComponent {
|
||||
function CardGrid_IntersectionObserver({
|
||||
children,
|
||||
windowObj = window,
|
||||
onIntersecting
|
||||
}) {
|
||||
const intersectionElement = (0,external_React_namespaceObject.useRef)(null);
|
||||
(0,external_React_namespaceObject.useEffect)(() => {
|
||||
let observer;
|
||||
|
||||
if (!observer && onIntersecting && intersectionElement.current) {
|
||||
observer = new windowObj.IntersectionObserver(entries => {
|
||||
const entry = entries.find(e => e.isIntersecting);
|
||||
|
||||
if (entry) {
|
||||
// Stop observing since element has been seen
|
||||
if (observer && intersectionElement.current) {
|
||||
observer.unobserve(intersectionElement.current);
|
||||
}
|
||||
|
||||
onIntersecting();
|
||||
}
|
||||
});
|
||||
observer.observe(intersectionElement.current);
|
||||
} // Cleanup
|
||||
|
||||
|
||||
return () => {
|
||||
var _observer;
|
||||
|
||||
return (_observer = observer) === null || _observer === void 0 ? void 0 : _observer.disconnect();
|
||||
};
|
||||
}, [windowObj, onIntersecting]);
|
||||
return /*#__PURE__*/external_React_default().createElement("div", {
|
||||
ref: intersectionElement
|
||||
}, children);
|
||||
}
|
||||
function RecentSavesContainer({
|
||||
className,
|
||||
dispatch,
|
||||
windowObj = window,
|
||||
items = 3
|
||||
}) {
|
||||
const {
|
||||
recentSavesData,
|
||||
isUserLoggedIn
|
||||
} = (0,external_ReactRedux_namespaceObject.useSelector)(state => state.DiscoveryStream);
|
||||
const [visible, setVisible] = (0,external_React_namespaceObject.useState)(false);
|
||||
const onIntersecting = (0,external_React_namespaceObject.useCallback)(() => setVisible(true), []);
|
||||
(0,external_React_namespaceObject.useEffect)(() => {
|
||||
if (visible) {
|
||||
dispatch(actionCreators.AlsoToMain({
|
||||
type: actionTypes.DISCOVERY_STREAM_POCKET_STATE_INIT
|
||||
}));
|
||||
}
|
||||
}, [visible, dispatch]); // The user has not yet scrolled to this section,
|
||||
// so wait before potentially requesting Pocket data.
|
||||
|
||||
if (!visible) {
|
||||
return /*#__PURE__*/external_React_default().createElement(CardGrid_IntersectionObserver, {
|
||||
windowObj: windowObj,
|
||||
onIntersecting: onIntersecting
|
||||
});
|
||||
} // Intersection observer has finished, but we're not yet logged in.
|
||||
|
||||
|
||||
if (visible && !isUserLoggedIn) {
|
||||
return null;
|
||||
}
|
||||
|
||||
function renderCard(rec, index) {
|
||||
return /*#__PURE__*/external_React_default().createElement(DSCard, {
|
||||
key: `dscard-${(rec === null || rec === void 0 ? void 0 : rec.id) || index}`,
|
||||
id: rec.id,
|
||||
pos: index,
|
||||
type: "CARDGRID_RECENT_SAVES",
|
||||
image_src: rec.image_src,
|
||||
raw_image_src: rec.raw_image_src,
|
||||
word_count: rec.word_count,
|
||||
time_to_read: rec.time_to_read,
|
||||
title: rec.title,
|
||||
excerpt: rec.excerpt,
|
||||
url: rec.url,
|
||||
source: rec.domain,
|
||||
isRecentSave: true,
|
||||
dispatch: dispatch
|
||||
});
|
||||
}
|
||||
|
||||
const recentSavesCards = []; // We fill the cards with a for loop over an inline map because
|
||||
// we want empty placeholders if there are not enough cards.
|
||||
|
||||
for (let index = 0; index < items; index++) {
|
||||
const recentSave = recentSavesData[index];
|
||||
|
||||
if (!recentSave) {
|
||||
recentSavesCards.push( /*#__PURE__*/external_React_default().createElement(PlaceholderDSCard, {
|
||||
key: `dscard-${index}`
|
||||
}));
|
||||
} else {
|
||||
var _recentSave$domain_me;
|
||||
|
||||
recentSavesCards.push(renderCard({
|
||||
id: recentSave.item_id || recentSave.resolved_id,
|
||||
image_src: recentSave.top_image_url,
|
||||
raw_image_src: recentSave.top_image_url,
|
||||
word_count: recentSave.word_count,
|
||||
time_to_read: recentSave.time_to_read,
|
||||
title: recentSave.resolved_title,
|
||||
url: recentSave.resolved_url,
|
||||
domain: (_recentSave$domain_me = recentSave.domain_metadata) === null || _recentSave$domain_me === void 0 ? void 0 : _recentSave$domain_me.name,
|
||||
excerpt: recentSave.excerpt
|
||||
}, index));
|
||||
}
|
||||
} // We are visible and logged in.
|
||||
|
||||
|
||||
return /*#__PURE__*/external_React_default().createElement(GridContainer, {
|
||||
className: className,
|
||||
header: "Recently Saved to your List"
|
||||
}, recentSavesCards);
|
||||
}
|
||||
class _CardGrid extends (external_React_default()).PureComponent {
|
||||
constructor(props) {
|
||||
super(props);
|
||||
this.state = {
|
||||
|
@ -8199,6 +8329,12 @@ class CardGrid extends (external_React_default()).PureComponent {
|
|||
let {
|
||||
items
|
||||
} = this.props;
|
||||
const {
|
||||
DiscoveryStream
|
||||
} = this.props;
|
||||
const {
|
||||
recentSavesEnabled
|
||||
} = DiscoveryStream;
|
||||
const {
|
||||
hybridLayout,
|
||||
hideCardBackground,
|
||||
|
@ -8312,20 +8448,25 @@ class CardGrid extends (external_React_default()).PureComponent {
|
|||
cards.splice(position.index, 1, widgetComponent);
|
||||
}
|
||||
}
|
||||
} // For now this is English only.
|
||||
}
|
||||
|
||||
let moreRecsHeader = ""; // For now this is English only.
|
||||
|
||||
if (essentialReadsHeader && editorsPicksHeader) {
|
||||
if (recentSavesEnabled || essentialReadsHeader && editorsPicksHeader) {
|
||||
let spliceAt = 6; // For 4 card row layouts, second row is 8 cards, and regular it is 6 cards.
|
||||
|
||||
if (fourCardLayout) {
|
||||
spliceAt = 8;
|
||||
} // Put the first 2 rows into essentialReadsCards.
|
||||
} // If we have a custom header, ensure the more recs section also has a header.
|
||||
|
||||
|
||||
moreRecsHeader = "More Recommendations"; // Put the first 2 rows into essentialReadsCards.
|
||||
|
||||
essentialReadsCards = [...cards.splice(0, spliceAt)]; // Put the rest into editorsPicksCards.
|
||||
|
||||
if (essentialReadsHeader && editorsPicksHeader) {
|
||||
editorsPicksCards = [...cards.splice(0, cards.length)];
|
||||
}
|
||||
} // Used for CSS overrides to default styling (eg: "hero")
|
||||
|
||||
|
||||
|
@ -8338,11 +8479,15 @@ class CardGrid extends (external_React_default()).PureComponent {
|
|||
const className = `ds-card-grid-${this.props.border} ${variantClass} ${hybridLayoutClassName} ${hideCardBackgroundClass} ${fourCardLayoutClass} ${hideDescriptionsClassName} ${compactGridClassName}`;
|
||||
return /*#__PURE__*/external_React_default().createElement((external_React_default()).Fragment, null, ((_essentialReadsCards = essentialReadsCards) === null || _essentialReadsCards === void 0 ? void 0 : _essentialReadsCards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
|
||||
className: className
|
||||
}, essentialReadsCards), ((_editorsPicksCards = editorsPicksCards) === null || _editorsPicksCards === void 0 ? void 0 : _editorsPicksCards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
|
||||
}, essentialReadsCards), recentSavesEnabled && /*#__PURE__*/external_React_default().createElement(RecentSavesContainer, {
|
||||
className: className,
|
||||
dispatch: this.props.dispatch
|
||||
}), ((_editorsPicksCards = editorsPicksCards) === null || _editorsPicksCards === void 0 ? void 0 : _editorsPicksCards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
|
||||
className: className,
|
||||
header: "Editor\u2019s Picks"
|
||||
}, editorsPicksCards), (cards === null || cards === void 0 ? void 0 : cards.length) > 0 && /*#__PURE__*/external_React_default().createElement(GridContainer, {
|
||||
className: className
|
||||
className: className,
|
||||
header: moreRecsHeader
|
||||
}, cards));
|
||||
}
|
||||
|
||||
|
@ -8379,7 +8524,7 @@ class CardGrid extends (external_React_default()).PureComponent {
|
|||
}
|
||||
|
||||
}
|
||||
CardGrid.defaultProps = {
|
||||
_CardGrid.defaultProps = {
|
||||
border: `border`,
|
||||
items: 4,
|
||||
// Number of stories to display
|
||||
|
@ -8388,6 +8533,9 @@ CardGrid.defaultProps = {
|
|||
saveToPocketCard: false,
|
||||
loadMoreThreshold: 12
|
||||
};
|
||||
const CardGrid = (0,external_ReactRedux_namespaceObject.connect)(state => ({
|
||||
DiscoveryStream: state.DiscoveryStream
|
||||
}))(_CardGrid);
|
||||
;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss.jsx
|
||||
/* 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,
|
||||
|
@ -10335,7 +10483,10 @@ const INITIAL_STATE = {
|
|||
utmSource: "pocket-newtab",
|
||||
utmCampaign: undefined,
|
||||
utmContent: undefined
|
||||
}
|
||||
},
|
||||
recentSavesData: [],
|
||||
isUserLoggedIn: false,
|
||||
recentSavesEnabled: false
|
||||
},
|
||||
Personalization: {
|
||||
lastUpdated: null,
|
||||
|
@ -10980,6 +11131,21 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
|
|||
isCollectionDismissible: action.data.value
|
||||
};
|
||||
|
||||
case actionTypes.DISCOVERY_STREAM_PREFS_SETUP:
|
||||
return { ...prevState,
|
||||
recentSavesEnabled: action.data.recentSavesEnabled
|
||||
};
|
||||
|
||||
case actionTypes.DISCOVERY_STREAM_RECENT_SAVES:
|
||||
return { ...prevState,
|
||||
recentSavesData: action.data.recentSaves
|
||||
};
|
||||
|
||||
case actionTypes.DISCOVERY_STREAM_POCKET_STATE_SET:
|
||||
return { ...prevState,
|
||||
isUserLoggedIn: action.data.isUserLoggedIn
|
||||
};
|
||||
|
||||
case actionTypes.HIDE_PRIVACY_INFO:
|
||||
return { ...prevState,
|
||||
isPrivacyInfoModalVisible: false
|
||||
|
|
|
@ -14,6 +14,11 @@ ChromeUtils.defineModuleGetter(
|
|||
"RemoteSettings",
|
||||
"resource://services-settings/remote-settings.js"
|
||||
);
|
||||
ChromeUtils.defineModuleGetter(
|
||||
lazy,
|
||||
"pktApi",
|
||||
"chrome://pocket/content/pktApi.jsm"
|
||||
);
|
||||
const { setTimeout, clearTimeout } = ChromeUtils.import(
|
||||
"resource://gre/modules/Timer.jsm"
|
||||
);
|
||||
|
@ -234,6 +239,18 @@ class DiscoveryStreamFeed {
|
|||
},
|
||||
})
|
||||
);
|
||||
const nimbusConfig = this.store.getState().Prefs.values?.pocketConfig || {};
|
||||
this.store.dispatch(
|
||||
ac.BroadcastToContent({
|
||||
type: at.DISCOVERY_STREAM_PREFS_SETUP,
|
||||
data: {
|
||||
recentSavesEnabled: nimbusConfig.recentSavesEnabled,
|
||||
},
|
||||
meta: {
|
||||
isStartup,
|
||||
},
|
||||
})
|
||||
);
|
||||
this.store.dispatch(
|
||||
ac.BroadcastToContent({
|
||||
type: at.DISCOVERY_STREAM_COLLECTION_DISMISSIBLE_TOGGLE,
|
||||
|
@ -249,6 +266,45 @@ class DiscoveryStreamFeed {
|
|||
);
|
||||
}
|
||||
|
||||
async setupPocketState(target) {
|
||||
let dispatch = action =>
|
||||
this.store.dispatch(ac.OnlyToOneContent(action, target));
|
||||
const isUserLoggedIn = lazy.pktApi.isUserLoggedIn();
|
||||
dispatch({
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_SET,
|
||||
data: {
|
||||
isUserLoggedIn,
|
||||
},
|
||||
});
|
||||
|
||||
// If we're not logged in, don't bother fetching recent saves, we're done.
|
||||
if (isUserLoggedIn) {
|
||||
let recentSaves = await lazy.pktApi.getRecentSavesCache();
|
||||
if (recentSaves) {
|
||||
// We have cache, so we can use those.
|
||||
dispatch({
|
||||
type: at.DISCOVERY_STREAM_RECENT_SAVES,
|
||||
data: {
|
||||
recentSaves,
|
||||
},
|
||||
});
|
||||
} else {
|
||||
// We don't have cache, so fetch fresh stories.
|
||||
lazy.pktApi.getRecentSaves({
|
||||
success(data) {
|
||||
dispatch({
|
||||
type: at.DISCOVERY_STREAM_RECENT_SAVES,
|
||||
data: {
|
||||
recentSaves: data,
|
||||
},
|
||||
});
|
||||
},
|
||||
error(error) {},
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
uninitPrefs() {
|
||||
// Reset in-memory cache
|
||||
this._prefCache = {};
|
||||
|
@ -1730,7 +1786,9 @@ class DiscoveryStreamFeed {
|
|||
)
|
||||
);
|
||||
break;
|
||||
|
||||
case at.DISCOVERY_STREAM_POCKET_STATE_INIT:
|
||||
this.setupPocketState(action.meta.fromTarget);
|
||||
break;
|
||||
case at.DISCOVERY_STREAM_CONFIG_RESET:
|
||||
// This is a generic config reset likely related to an external feed pref.
|
||||
this.configReset();
|
||||
|
|
|
@ -982,6 +982,27 @@ describe("Reducers", () => {
|
|||
});
|
||||
assert.deepEqual(state.config, { enabled: true });
|
||||
});
|
||||
it("should set recentSavesEnabled with DISCOVERY_STREAM_PREFS_SETUP", () => {
|
||||
const state = DiscoveryStream(undefined, {
|
||||
type: at.DISCOVERY_STREAM_PREFS_SETUP,
|
||||
data: { recentSavesEnabled: true },
|
||||
});
|
||||
assert.isTrue(state.recentSavesEnabled);
|
||||
});
|
||||
it("should set recentSavesData with DISCOVERY_STREAM_RECENT_SAVES", () => {
|
||||
const state = DiscoveryStream(undefined, {
|
||||
type: at.DISCOVERY_STREAM_RECENT_SAVES,
|
||||
data: { recentSaves: [1, 2, 3] },
|
||||
});
|
||||
assert.deepEqual(state.recentSavesData, [1, 2, 3]);
|
||||
});
|
||||
it("should set isUserLoggedIn with DISCOVERY_STREAM_POCKET_STATE_SET", () => {
|
||||
const state = DiscoveryStream(undefined, {
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_SET,
|
||||
data: { isUserLoggedIn: true },
|
||||
});
|
||||
assert.isTrue(state.isUserLoggedIn);
|
||||
});
|
||||
it("should set feeds as loaded with DISCOVERY_STREAM_FEEDS_UPDATE", () => {
|
||||
const state = DiscoveryStream(undefined, {
|
||||
type: at.DISCOVERY_STREAM_FEEDS_UPDATE,
|
||||
|
|
|
@ -1,5 +1,7 @@
|
|||
import {
|
||||
CardGrid,
|
||||
_CardGrid as CardGrid,
|
||||
IntersectionObserver,
|
||||
RecentSavesContainer,
|
||||
DSSubHeader,
|
||||
GridContainer,
|
||||
} from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
|
||||
|
@ -8,18 +10,28 @@ import { INITIAL_STATE, reducers } from "common/Reducers.jsm";
|
|||
import { Provider } from "react-redux";
|
||||
import {
|
||||
DSCard,
|
||||
PlaceholderDSCard,
|
||||
LastCardMessage,
|
||||
} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard";
|
||||
import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget";
|
||||
import { actionCreators as ac } from "common/Actions.jsm";
|
||||
import { actionCreators as ac, actionTypes as at } from "common/Actions.jsm";
|
||||
import React from "react";
|
||||
import { shallow, mount } from "enzyme";
|
||||
|
||||
// Wrap this around any component that uses useSelector,
|
||||
// or any mount that uses a child that uses redux.
|
||||
function WrapWithProvider({ children, state = INITIAL_STATE }) {
|
||||
let store = createStore(combineReducers(reducers), state);
|
||||
return <Provider store={store}>{children}</Provider>;
|
||||
}
|
||||
|
||||
describe("<CardGrid>", () => {
|
||||
let wrapper;
|
||||
|
||||
beforeEach(() => {
|
||||
wrapper = shallow(<CardGrid />);
|
||||
wrapper = shallow(
|
||||
<CardGrid DiscoveryStream={INITIAL_STATE.DiscoveryStream} />
|
||||
);
|
||||
});
|
||||
|
||||
it("should render an empty div", () => {
|
||||
|
@ -69,9 +81,8 @@ describe("<CardGrid>", () => {
|
|||
});
|
||||
|
||||
it("should render sub header in the middle of the card grid for both regular and compact", () => {
|
||||
let store = createStore(combineReducers(reducers), INITIAL_STATE);
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<WrapWithProvider>
|
||||
<CardGrid
|
||||
essentialReadsHeader={true}
|
||||
editorsPicksHeader={true}
|
||||
|
@ -79,8 +90,9 @@ describe("<CardGrid>", () => {
|
|||
data={{
|
||||
recommendations: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
|
||||
}}
|
||||
DiscoveryStream={INITIAL_STATE.DiscoveryStream}
|
||||
/>
|
||||
</Provider>
|
||||
</WrapWithProvider>
|
||||
);
|
||||
|
||||
assert.ok(wrapper.find(DSSubHeader).exists());
|
||||
|
@ -89,7 +101,7 @@ describe("<CardGrid>", () => {
|
|||
compact: true,
|
||||
});
|
||||
wrapper = mount(
|
||||
<Provider store={store}>
|
||||
<WrapWithProvider>
|
||||
<CardGrid
|
||||
essentialReadsHeader={true}
|
||||
editorsPicksHeader={true}
|
||||
|
@ -98,8 +110,9 @@ describe("<CardGrid>", () => {
|
|||
data={{
|
||||
recommendations: [{}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}, {}],
|
||||
}}
|
||||
DiscoveryStream={INITIAL_STATE.DiscoveryStream}
|
||||
/>
|
||||
</Provider>
|
||||
</WrapWithProvider>
|
||||
);
|
||||
|
||||
assert.ok(wrapper.find(DSSubHeader).exists());
|
||||
|
@ -184,3 +197,132 @@ describe("<CardGrid>", () => {
|
|||
assert.ok(wrapper.find(TopicsWidget).exists());
|
||||
});
|
||||
});
|
||||
|
||||
// Build IntersectionObserver class with the arg `entries` for the intersect callback.
|
||||
function buildIntersectionObserver(entries) {
|
||||
return class {
|
||||
constructor(callback) {
|
||||
this.callback = callback;
|
||||
}
|
||||
|
||||
observe() {
|
||||
this.callback(entries);
|
||||
}
|
||||
|
||||
unobserve() {}
|
||||
|
||||
disconnect() {}
|
||||
};
|
||||
}
|
||||
|
||||
describe("<IntersectionObserver>", () => {
|
||||
let wrapper;
|
||||
let fakeWindow;
|
||||
let intersectEntries;
|
||||
|
||||
beforeEach(() => {
|
||||
intersectEntries = [{ isIntersecting: true }];
|
||||
fakeWindow = {
|
||||
IntersectionObserver: buildIntersectionObserver(intersectEntries),
|
||||
};
|
||||
wrapper = mount(<IntersectionObserver windowObj={fakeWindow} />);
|
||||
});
|
||||
|
||||
it("should render an empty div", () => {
|
||||
assert.ok(wrapper.exists());
|
||||
assert.equal(
|
||||
wrapper
|
||||
.children()
|
||||
.at(0)
|
||||
.type(),
|
||||
"div"
|
||||
);
|
||||
});
|
||||
|
||||
it("should fire onIntersecting", () => {
|
||||
const onIntersecting = sinon.stub();
|
||||
wrapper = mount(
|
||||
<IntersectionObserver
|
||||
windowObj={fakeWindow}
|
||||
onIntersecting={onIntersecting}
|
||||
/>
|
||||
);
|
||||
assert.calledOnce(onIntersecting);
|
||||
});
|
||||
});
|
||||
|
||||
describe("<RecentSavesContainer>", () => {
|
||||
let wrapper;
|
||||
let fakeWindow;
|
||||
let intersectEntries;
|
||||
let dispatch;
|
||||
|
||||
beforeEach(() => {
|
||||
dispatch = sinon.stub();
|
||||
intersectEntries = [{ isIntersecting: false }];
|
||||
fakeWindow = {
|
||||
IntersectionObserver: buildIntersectionObserver(intersectEntries),
|
||||
};
|
||||
wrapper = mount(
|
||||
<WrapWithProvider>
|
||||
<RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
|
||||
</WrapWithProvider>
|
||||
).find(RecentSavesContainer);
|
||||
});
|
||||
|
||||
it("should render an IntersectionObserver when not visible", () => {
|
||||
assert.ok(wrapper.exists());
|
||||
assert.ok(wrapper.find(IntersectionObserver).exists());
|
||||
});
|
||||
|
||||
it("should render a nothing if visible until we log in", () => {
|
||||
intersectEntries = [{ isIntersecting: true }];
|
||||
fakeWindow = {
|
||||
IntersectionObserver: buildIntersectionObserver(intersectEntries),
|
||||
};
|
||||
wrapper = mount(
|
||||
<WrapWithProvider>
|
||||
<RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
|
||||
</WrapWithProvider>
|
||||
).find(RecentSavesContainer);
|
||||
assert.ok(!wrapper.find(IntersectionObserver).exists());
|
||||
assert.calledOnce(dispatch);
|
||||
assert.calledWith(
|
||||
dispatch,
|
||||
ac.AlsoToMain({
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it("should render a GridContainer if visible and logged in", () => {
|
||||
intersectEntries = [{ isIntersecting: true }];
|
||||
fakeWindow = {
|
||||
IntersectionObserver: buildIntersectionObserver(intersectEntries),
|
||||
};
|
||||
wrapper = mount(
|
||||
<WrapWithProvider
|
||||
state={{
|
||||
DiscoveryStream: {
|
||||
isUserLoggedIn: true,
|
||||
recentSavesData: [
|
||||
{
|
||||
resolved_id: "resolved_id",
|
||||
top_image_url: "top_image_url",
|
||||
title: "title",
|
||||
resolved_url: "resolved_url",
|
||||
domain: "domain",
|
||||
excerpt: "excerpt",
|
||||
},
|
||||
],
|
||||
},
|
||||
}}
|
||||
>
|
||||
<RecentSavesContainer windowObj={fakeWindow} dispatch={dispatch} />
|
||||
</WrapWithProvider>
|
||||
).find(RecentSavesContainer);
|
||||
assert.lengthOf(wrapper.find(GridContainer), 1);
|
||||
assert.lengthOf(wrapper.find(PlaceholderDSCard), 2);
|
||||
assert.lengthOf(wrapper.find(DSCard), 3);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -31,6 +31,7 @@ describe("DiscoveryStreamFeed", () => {
|
|||
let fetchStub;
|
||||
let clock;
|
||||
let fakeNewTabUtils;
|
||||
let fakePktApi;
|
||||
let globals;
|
||||
|
||||
const setPref = (name, value) => {
|
||||
|
@ -109,6 +110,13 @@ describe("DiscoveryStreamFeed", () => {
|
|||
},
|
||||
};
|
||||
globals.set("NewTabUtils", fakeNewTabUtils);
|
||||
|
||||
fakePktApi = {
|
||||
isUserLoggedIn: () => false,
|
||||
getRecentSavesCache: () => null,
|
||||
getRecentSaves: () => null,
|
||||
};
|
||||
globals.set("pktApi", fakePktApi);
|
||||
});
|
||||
|
||||
afterEach(() => {
|
||||
|
@ -211,6 +219,63 @@ describe("DiscoveryStreamFeed", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#setupPocketState", () => {
|
||||
it("should setup logged in state and recent saves with cache", async () => {
|
||||
fakePktApi.isUserLoggedIn = () => true;
|
||||
fakePktApi.getRecentSavesCache = () => [1, 2, 3];
|
||||
sandbox.spy(feed.store, "dispatch");
|
||||
await feed.setupPocketState({});
|
||||
assert.calledTwice(feed.store.dispatch);
|
||||
assert.calledWith(
|
||||
feed.store.dispatch.firstCall,
|
||||
ac.OnlyToOneContent(
|
||||
{
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_SET,
|
||||
data: { isUserLoggedIn: true },
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.calledWith(
|
||||
feed.store.dispatch.secondCall,
|
||||
ac.OnlyToOneContent(
|
||||
{
|
||||
type: at.DISCOVERY_STREAM_RECENT_SAVES,
|
||||
data: { recentSaves: [1, 2, 3] },
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
});
|
||||
it("should setup logged in state and recent saves without cache", async () => {
|
||||
fakePktApi.isUserLoggedIn = () => true;
|
||||
fakePktApi.getRecentSaves = ({ success }) => success([1, 2, 3]);
|
||||
sandbox.spy(feed.store, "dispatch");
|
||||
await feed.setupPocketState({});
|
||||
assert.calledTwice(feed.store.dispatch);
|
||||
assert.calledWith(
|
||||
feed.store.dispatch.firstCall,
|
||||
ac.OnlyToOneContent(
|
||||
{
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_SET,
|
||||
data: { isUserLoggedIn: true },
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
assert.calledWith(
|
||||
feed.store.dispatch.secondCall,
|
||||
ac.OnlyToOneContent(
|
||||
{
|
||||
type: at.DISCOVERY_STREAM_RECENT_SAVES,
|
||||
data: { recentSaves: [1, 2, 3] },
|
||||
},
|
||||
{}
|
||||
)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getOrCreateImpressionId", () => {
|
||||
it("should create impression id in constructor", async () => {
|
||||
assert.equal(feed._impressionId, FAKE_UUID);
|
||||
|
@ -2002,6 +2067,17 @@ describe("DiscoveryStreamFeed", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#onAction: DISCOVERY_STREAM_POCKET_STATE_INIT", async () => {
|
||||
it("should call setupPocketState", async () => {
|
||||
sandbox.spy(feed, "setupPocketState");
|
||||
feed.onAction({
|
||||
type: at.DISCOVERY_STREAM_POCKET_STATE_INIT,
|
||||
meta: { fromTarget: {} },
|
||||
});
|
||||
assert.calledOnce(feed.setupPocketState);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onAction: DISCOVERY_STREAM_CONFIG_RESET", async () => {
|
||||
it("should call configReset", async () => {
|
||||
sandbox.spy(feed, "configReset");
|
||||
|
|
|
@ -335,6 +335,12 @@ pocketNewtab:
|
|||
description: >-
|
||||
Updates the Pocket section header and title to say "Editor’s Picks", if used with
|
||||
essentialReadsHeader, creates a second section 2 rows down for editorsPicksHeader.
|
||||
recentSavesEnabled:
|
||||
type: boolean
|
||||
fallbackPref: >-
|
||||
browser.newtabpage.activity-stream.discoverystream.recentSaves.enabled
|
||||
description: >-
|
||||
Updates the Pocket section with a new header and 1 row of recently saved Pocket stories.
|
||||
readTime:
|
||||
type: boolean
|
||||
fallbackPref: >-
|
||||
|
|
Загрузка…
Ссылка в новой задаче