Bug 1767445 - Pocket newtab topics widget r=gvn

Differential Revision: https://phabricator.services.mozilla.com/D146327
This commit is contained in:
Scott 2022-05-20 22:04:46 +00:00
Родитель 7c5eeccac8
Коммит 49ba6a6e01
12 изменённых файлов: 172 добавлений и 11 удалений

Просмотреть файл

@ -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.essentialReadsHeader.enabled", false);
pref("browser.newtabpage.activity-stream.discoverystream.editorsPicksHeader.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.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", "");
pref("browser.newtabpage.activity-stream.discoverystream.spocs-endpoint-query", ""); pref("browser.newtabpage.activity-stream.discoverystream.spocs-endpoint-query", "");

Просмотреть файл

@ -207,6 +207,7 @@ export class _DiscoveryStreamBase extends React.PureComponent {
display_variant={component.properties.display_variant} display_variant={component.properties.display_variant}
data={component.data} data={component.data}
feed={component.feed} feed={component.feed}
widgets={component.widgets}
border={component.properties.border} border={component.properties.border}
type={component.type} type={component.type}
dispatch={this.props.dispatch} dispatch={this.props.dispatch}

Просмотреть файл

@ -8,6 +8,7 @@ import {
LastCardMessage, LastCardMessage,
} from "../DSCard/DSCard.jsx"; } from "../DSCard/DSCard.jsx";
import { DSEmptyState } from "../DSEmptyState/DSEmptyState.jsx"; import { DSEmptyState } from "../DSEmptyState/DSEmptyState.jsx";
import { TopicsWidget } from "../TopicsWidget/TopicsWidget.jsx";
import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx"; import { FluentOrText } from "../../FluentOrText/FluentOrText.jsx";
import { actionCreators as ac } from "common/Actions.jsm"; import { actionCreators as ac } from "common/Actions.jsm";
import React from "react"; import React from "react";
@ -69,6 +70,7 @@ export class CardGrid extends React.PureComponent {
readTime, readTime,
essentialReadsHeader, essentialReadsHeader,
editorsPicksHeader, editorsPicksHeader,
widgets,
} = this.props; } = this.props;
let showLastCardMessage = lastCardMessageEnabled; let showLastCardMessage = lastCardMessageEnabled;
if (this.showLoadMore) { 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 = <TopicsWidget />;
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") // Used for CSS overrides to default styling (eg: "hero")
const variantClass = this.props.display_variant const variantClass = this.props.display_variant
? `ds-card-grid-${this.props.display_variant}` ? `ds-card-grid-${this.props.display_variant}`

Просмотреть файл

@ -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 <div className="ds-topics-widget"></div>;
}

Просмотреть файл

@ -167,6 +167,7 @@ input {
@import '../components/DiscoveryStreamComponents/DSSignup/DSSignup'; @import '../components/DiscoveryStreamComponents/DSSignup/DSSignup';
@import '../components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal'; @import '../components/DiscoveryStreamComponents/DSPrivacyModal/DSPrivacyModal';
@import '../components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink'; @import '../components/DiscoveryStreamComponents/PrivacyLink/PrivacyLink';
@import '../components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget';
// AS Router // AS Router
@import '../asrouter/components/Button/Button'; @import '../asrouter/components/Button/Button';

Просмотреть файл

@ -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 ;// CONCATENATED MODULE: ./content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx
/* This Source Code Form is subject to the terms of the Mozilla Public /* 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, * 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 { class CardGrid extends (external_React_default()).PureComponent {
constructor(props) { constructor(props) {
super(props); super(props);
@ -8059,6 +8070,8 @@ class CardGrid extends (external_React_default()).PureComponent {
} }
renderCards() { renderCards() {
var _widgets$positions, _widgets$data;
let { let {
items items
} = this.props; } = this.props;
@ -8078,7 +8091,8 @@ class CardGrid extends (external_React_default()).PureComponent {
descLines, descLines,
readTime, readTime,
essentialReadsHeader, essentialReadsHeader,
editorsPicksHeader editorsPicksHeader,
widgets
} = this.props; } = this.props;
let showLastCardMessage = lastCardMessageEnabled; 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, { cards.splice(cards.length - 1, 1, /*#__PURE__*/external_React_default().createElement(LastCardMessage, {
key: `dscard-last-${cards.length - 1}` 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") } // 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, display_variant: component.properties.display_variant,
data: component.data, data: component.data,
feed: component.feed, feed: component.feed,
widgets: component.widgets,
border: component.properties.border, border: component.properties.border,
type: component.type, type: component.type,
dispatch: this.props.dispatch, dispatch: this.props.dispatch,

Просмотреть файл

@ -418,12 +418,12 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
return urlObject.toString(); return urlObject.toString();
} }
parseSpocPositions(csvPositions) { parseGridPositions(csvPositions) {
let spocPositions; let gridPositions;
// Only accept parseable non-negative integers // Only accept parseable non-negative integers
try { try {
spocPositions = csvPositions.map(index => { gridPositions = csvPositions.map(index => {
let parsedInt = parseInt(index, 10); let parsedInt = parseInt(index, 10);
if (!isNaN(parsedInt) && parsedInt >= 0) { if (!isNaN(parsedInt) && parsedInt >= 0) {
@ -435,10 +435,10 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
} catch (e) { } catch (e) {
// Catch spoc positions that are not numbers or negative, and do nothing. // Catch spoc positions that are not numbers or negative, and do nothing.
// We have hard coded backup positions. // We have hard coded backup positions.
spocPositions = undefined; gridPositions = undefined;
} }
return spocPositions; return gridPositions;
} }
async loadLayout(sendUpdate, isStartup) { async loadLayout(sendUpdate, isStartup) {
@ -480,9 +480,15 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
layoutResp = getHardcodedLayout({ layoutResp = getHardcodedLayout({
items, items,
sponsoredCollectionsEnabled, sponsoredCollectionsEnabled,
spocPositions: this.parseSpocPositions( spocPositions: this.parseGridPositions(
pocketConfig.spocPositions?.split(`,`) pocketConfig.spocPositions?.split(`,`)
), ),
widgetPositions: this.parseGridPositions(
pocketConfig.widgetPositions?.split(`,`)
),
widgetData: [
...(this.locale.startsWith("en-") ? [{ type: "TopicsWidget" }] : []),
],
compactLayout: pocketConfig.compactLayout, compactLayout: pocketConfig.compactLayout,
hybridLayout: pocketConfig.hybridLayout, hybridLayout: pocketConfig.hybridLayout,
hideCardBackground: pocketConfig.hideCardBackground, hideCardBackground: pocketConfig.hideCardBackground,
@ -1906,6 +1912,8 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
getHardcodedLayout = ({ getHardcodedLayout = ({
items = 21, items = 21,
spocPositions = [1, 5, 7, 11, 18, 20], spocPositions = [1, 5, 7, 11, 18, 20],
widgetPositions = [],
widgetData = [],
sponsoredCollectionsEnabled = false, sponsoredCollectionsEnabled = false,
compactLayout = false, compactLayout = false,
hybridLayout = false, hybridLayout = false,
@ -2016,6 +2024,12 @@ getHardcodedLayout = ({
editorsPicksHeader, editorsPicksHeader,
readTime: readTime || compactLayout, readTime: readTime || compactLayout,
}, },
widgets: {
positions: widgetPositions.map(position => {
return { index: position };
}),
data: widgetData,
},
loadMore, loadMore,
lastCardMessageEnabled, lastCardMessageEnabled,
pocketButtonEnabled, pocketButtonEnabled,

Просмотреть файл

@ -3,6 +3,7 @@ import {
DSCard, DSCard,
LastCardMessage, LastCardMessage,
} from "content-src/components/DiscoveryStreamComponents/DSCard/DSCard"; } 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 } from "common/Actions.jsm";
import React from "react"; import React from "react";
import { shallow } from "enzyme"; import { shallow } from "enzyme";
@ -140,4 +141,18 @@ describe("<CardGrid>", () => {
loadMoreButton = wrapper.find(".ds-card-grid-load-more-button"); loadMoreButton = wrapper.find(".ds-card-grid-load-more-button");
assert.ok(!loadMoreButton.exists()); 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());
});
}); });

Просмотреть файл

@ -0,0 +1,21 @@
import { TopicsWidget } from "content-src/components/DiscoveryStreamComponents/TopicsWidget/TopicsWidget";
import { shallow } from "enzyme";
import React from "react";
describe("Discovery Stream <TopicsWidget>", () => {
let sandbox;
let wrapper;
beforeEach(() => {
sandbox = sinon.createSandbox();
wrapper = shallow(<TopicsWidget />);
});
afterEach(() => {
sandbox.restore();
});
it("should render", () => {
assert.ok(wrapper.exists());
assert.ok(wrapper.find(".ds-topics-widget"));
});
});

Просмотреть файл

@ -226,15 +226,15 @@ describe("DiscoveryStreamFeed", () => {
}); });
}); });
describe("#parseSpocPositions", () => { describe("#parseGridPositions", () => {
it("should return an equivalent array for an array of non negative integers", async () => { 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 () => { 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 () => { 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; const { layout } = feed.store.getState().DiscoveryStream;
assert.equal(layout[0].components[2].properties.items, 24); 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", () => { describe("#updatePlacements", () => {

Просмотреть файл

@ -234,6 +234,10 @@ pocketNewtab:
type: string type: string
fallbackPref: browser.newtabpage.activity-stream.discoverystream.spoc-positions fallbackPref: browser.newtabpage.activity-stream.discoverystream.spoc-positions
description: CSV string of spoc position indexes on newtab grid 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: compactLayout:
type: boolean type: boolean
fallbackPref: browser.newtabpage.activity-stream.discoverystream.compactLayout.enabled fallbackPref: browser.newtabpage.activity-stream.discoverystream.compactLayout.enabled