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