diff --git a/browser/app/profile/firefox.js b/browser/app/profile/firefox.js index 961c607ed185..3eb614aff096 100644 --- a/browser/app/profile/firefox.js +++ b/browser/app/profile/firefox.js @@ -1552,6 +1552,7 @@ pref("browser.newtabpage.activity-stream.discoverystream.newSponsoredLabel.enabl pref("browser.newtabpage.activity-stream.discoverystream.essentialReadsHeader.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", ""); pref("browser.newtabpage.activity-stream.discoverystream.spocs-endpoint", ""); pref("browser.newtabpage.activity-stream.discoverystream.spocs-endpoint-query", ""); diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx index 79468a14e78a..e8455e608b6e 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx @@ -207,6 +207,7 @@ export class _DiscoveryStreamBase extends React.PureComponent { display_variant={component.properties.display_variant} data={component.data} feed={component.feed} + widgets={component.widgets} border={component.properties.border} type={component.type} dispatch={this.props.dispatch} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx index 2deef7118e19..3e78bdc35527 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx @@ -8,6 +8,7 @@ import { LastCardMessage, } from "../DSCard/DSCard.jsx"; 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"; @@ -69,6 +70,7 @@ export class CardGrid extends React.PureComponent { readTime, essentialReadsHeader, editorsPicksHeader, + widgets, } = this.props; let showLastCardMessage = lastCardMessageEnabled; if (this.showLoadMore) { @@ -148,6 +150,33 @@ export class CardGrid extends React.PureComponent { ); } + if (widgets?.positions?.length && widgets?.data?.length) { + let positionIndex = 0; + + for (const widget of widgets.data) { + let widgetComponent = null; + const position = widgets.positions[positionIndex]; + + // Stop if we run out of positions to place widgets. + if (!position) { + break; + } + + switch (widget?.type) { + case "TopicsWidget": + widgetComponent = ; + break; + } + + if (widgetComponent) { + // We found a widget, so up the position for next try. + positionIndex++; + // We replace an existing card with the widget. + cards.splice(position.index, 1, widgetComponent); + } + } + } + // Used for CSS overrides to default styling (eg: "hero") const variantClass = this.props.display_variant ? `ds-card-grid-${this.props.display_variant}` diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx new file mode 100644 index 000000000000..e0ae22341101 --- /dev/null +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.jsx @@ -0,0 +1,9 @@ +/* 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 React from "react"; + +export function TopicsWidget(props) { + return
; +} diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/TopicsWidget/_TopicsWidget.scss new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/browser/components/newtab/content-src/styles/_activity-stream.scss b/browser/components/newtab/content-src/styles/_activity-stream.scss index b6cc8d3841ab..42f349044b45 100644 --- a/browser/components/newtab/content-src/styles/_activity-stream.scss +++ b/browser/components/newtab/content-src/styles/_activity-stream.scss @@ -167,6 +167,7 @@ input { @import '../components/DiscoveryStreamComponents/DSSignup/DSSignup'; @import '../components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal'; @import '../components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink'; +@import '../components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget'; // AS Router @import '../asrouter/components/Button/Button'; diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js index 31cecd21518e..34395476104e 100644 --- a/browser/components/newtab/data/content/activity-stream.bundle.js +++ b/browser/components/newtab/data/content/activity-stream.bundle.js @@ -8009,6 +8009,16 @@ class DSEmptyState extends (external_React_default()).PureComponent { } } +;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget.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, + * You can obtain one at http://mozilla.org/MPL/2.0/. */ + +function TopicsWidget(props) { + return /*#__PURE__*/external_React_default().createElement("div", { + className: "ds-topics-widget" + }); +} ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.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, @@ -8018,6 +8028,7 @@ class DSEmptyState extends (external_React_default()).PureComponent { + class CardGrid extends (external_React_default()).PureComponent { constructor(props) { super(props); @@ -8059,6 +8070,8 @@ class CardGrid extends (external_React_default()).PureComponent { } renderCards() { + var _widgets$positions, _widgets$data; + let { items } = this.props; @@ -8078,7 +8091,8 @@ class CardGrid extends (external_React_default()).PureComponent { descLines, readTime, essentialReadsHeader, - editorsPicksHeader + editorsPicksHeader, + widgets } = this.props; let showLastCardMessage = lastCardMessageEnabled; @@ -8151,6 +8165,32 @@ class CardGrid extends (external_React_default()).PureComponent { cards.splice(cards.length - 1, 1, /*#__PURE__*/external_React_default().createElement(LastCardMessage, { key: `dscard-last-${cards.length - 1}` })); + } + + if (widgets !== null && widgets !== void 0 && (_widgets$positions = widgets.positions) !== null && _widgets$positions !== void 0 && _widgets$positions.length && widgets !== null && widgets !== void 0 && (_widgets$data = widgets.data) !== null && _widgets$data !== void 0 && _widgets$data.length) { + let positionIndex = 0; + + for (const widget of widgets.data) { + let widgetComponent = null; + const position = widgets.positions[positionIndex]; // Stop if we run out of positions to place widgets. + + if (!position) { + break; + } + + switch (widget === null || widget === void 0 ? void 0 : widget.type) { + case "TopicsWidget": + widgetComponent = /*#__PURE__*/external_React_default().createElement(TopicsWidget, null); + break; + } + + if (widgetComponent) { + // We found a widget, so up the position for next try. + positionIndex++; // We replace an existing card with the widget. + + cards.splice(position.index, 1, widgetComponent); + } + } } // Used for CSS overrides to default styling (eg: "hero") @@ -13531,6 +13571,7 @@ class _DiscoveryStreamBase extends (external_React_default()).PureComponent { display_variant: component.properties.display_variant, data: component.data, feed: component.feed, + widgets: component.widgets, border: component.properties.border, type: component.type, dispatch: this.props.dispatch, diff --git a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm index b2ff84d54965..ddedf429faa0 100644 --- a/browser/components/newtab/lib/DiscoveryStreamFeed.jsm +++ b/browser/components/newtab/lib/DiscoveryStreamFeed.jsm @@ -418,12 +418,12 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed { return urlObject.toString(); } - parseSpocPositions(csvPositions) { - let spocPositions; + parseGridPositions(csvPositions) { + let gridPositions; // Only accept parseable non-negative integers try { - spocPositions = csvPositions.map(index => { + gridPositions = csvPositions.map(index => { let parsedInt = parseInt(index, 10); if (!isNaN(parsedInt) && parsedInt >= 0) { @@ -435,10 +435,10 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed { } catch (e) { // Catch spoc positions that are not numbers or negative, and do nothing. // We have hard coded backup positions. - spocPositions = undefined; + gridPositions = undefined; } - return spocPositions; + return gridPositions; } async loadLayout(sendUpdate, isStartup) { @@ -480,9 +480,15 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed { layoutResp = getHardcodedLayout({ items, sponsoredCollectionsEnabled, - spocPositions: this.parseSpocPositions( + spocPositions: this.parseGridPositions( pocketConfig.spocPositions?.split(`,`) ), + widgetPositions: this.parseGridPositions( + pocketConfig.widgetPositions?.split(`,`) + ), + widgetData: [ + ...(this.locale.startsWith("en-") ? [{ type: "TopicsWidget" }] : []), + ], compactLayout: pocketConfig.compactLayout, hybridLayout: pocketConfig.hybridLayout, hideCardBackground: pocketConfig.hideCardBackground, @@ -1906,6 +1912,8 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed { getHardcodedLayout = ({ items = 21, spocPositions = [1, 5, 7, 11, 18, 20], + widgetPositions = [], + widgetData = [], sponsoredCollectionsEnabled = false, compactLayout = false, hybridLayout = false, @@ -2016,6 +2024,12 @@ getHardcodedLayout = ({ editorsPicksHeader, readTime: readTime || compactLayout, }, + widgets: { + positions: widgetPositions.map(position => { + return { index: position }; + }), + data: widgetData, + }, loadMore, lastCardMessageEnabled, pocketButtonEnabled, diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx index 02e10734f200..be42384636a0 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/CardGrid.test.jsx @@ -3,6 +3,7 @@ import { DSCard, 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 React from "react"; import { shallow } from "enzyme"; @@ -140,4 +141,18 @@ describe("", () => { loadMoreButton = wrapper.find(".ds-card-grid-load-more-button"); assert.ok(!loadMoreButton.exists()); }); + + it("should create a widget card", () => { + wrapper.setProps({ + widgets: { + positions: [{ index: 1 }], + data: [{ type: "TopicsWidget" }], + }, + data: { + recommendations: [{}, {}, {}], + }, + }); + + assert.ok(wrapper.find(TopicsWidget).exists()); + }); }); diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx new file mode 100644 index 000000000000..d8a031e9f97f --- /dev/null +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/TopicsWidget.test.jsx @@ -0,0 +1,21 @@ +import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget"; +import { shallow } from "enzyme"; +import React from "react"; + +describe("Discovery Stream ", () => { + let sandbox; + let wrapper; + beforeEach(() => { + sandbox = sinon.createSandbox(); + wrapper = shallow(); + }); + + afterEach(() => { + sandbox.restore(); + }); + + it("should render", () => { + assert.ok(wrapper.exists()); + assert.ok(wrapper.find(".ds-topics-widget")); + }); +}); diff --git a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js index 4acb25016ceb..1f1d29764a29 100644 --- a/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js +++ b/browser/components/newtab/test/unit/lib/DiscoveryStreamFeed.test.js @@ -226,15 +226,15 @@ describe("DiscoveryStreamFeed", () => { }); }); - describe("#parseSpocPositions", () => { + describe("#parseGridPositions", () => { it("should return an equivalent array for an array of non negative integers", async () => { - assert.deepEqual(feed.parseSpocPositions([0, 2, 3]), [0, 2, 3]); + assert.deepEqual(feed.parseGridPositions([0, 2, 3]), [0, 2, 3]); }); it("should return undefined for an array containing negative integers", async () => { - assert.equal(feed.parseSpocPositions([-2, 2, 3]), undefined); + assert.equal(feed.parseGridPositions([-2, 2, 3]), undefined); }); it("should return undefined for an undefined input", async () => { - assert.equal(feed.parseSpocPositions(undefined), undefined); + assert.equal(feed.parseGridPositions(undefined), undefined); }); }); @@ -466,6 +466,31 @@ describe("DiscoveryStreamFeed", () => { const { layout } = feed.store.getState().DiscoveryStream; assert.equal(layout[0].components[2].properties.items, 24); }); + it("should create a layout with spoc and widget positions", async () => { + feed.config.hardcoded_layout = true; + feed.store = createStore(combineReducers(reducers), { + Prefs: { + values: { + pocketConfig: { + spocPositions: "1, 2", + widgetPositions: "3, 4", + }, + }, + }, + }); + + await feed.loadLayout(feed.store.dispatch); + + const { layout } = feed.store.getState().DiscoveryStream; + assert.deepEqual(layout[0].components[2].spocs.positions, [ + { index: 1 }, + { index: 2 }, + ]); + assert.deepEqual(layout[0].components[2].widgets.positions, [ + { index: 3 }, + { index: 4 }, + ]); + }); }); describe("#updatePlacements", () => { diff --git a/toolkit/components/nimbus/FeatureManifest.yaml b/toolkit/components/nimbus/FeatureManifest.yaml index cb0da5b7bd26..5abe979b13fa 100644 --- a/toolkit/components/nimbus/FeatureManifest.yaml +++ b/toolkit/components/nimbus/FeatureManifest.yaml @@ -234,6 +234,10 @@ pocketNewtab: type: string fallbackPref: browser.newtabpage.activity-stream.discoverystream.spoc-positions description: CSV string of spoc position indexes on newtab grid + widgetPositions: + type: string + fallbackPref: browser.newtabpage.activity-stream.discoverystream.widget-positions + description: CSV string of widget position indexes on newtab grid compactLayout: type: boolean fallbackPref: browser.newtabpage.activity-stream.discoverystream.compactLayout.enabled