Skip to content
This repository has been archived by the owner on Feb 29, 2020. It is now read-only.

Commit

Permalink
Bug 1566653 - Topsites promo back end. (#5250)
Browse files Browse the repository at this point in the history
* Bug 1566653 - Topsites promo back end.
  • Loading branch information
ScottDowne committed Aug 28, 2019
1 parent cd3914f commit 5fb6a1a
Show file tree
Hide file tree
Showing 17 changed files with 1,012 additions and 270 deletions.
1 change: 1 addition & 0 deletions common/Actions.jsm
Expand Up @@ -57,6 +57,7 @@ for (const type of [
"DISCOVERY_STREAM_SPOCS_CAPS",
"DISCOVERY_STREAM_SPOCS_ENDPOINT",
"DISCOVERY_STREAM_SPOCS_FILL",
"DISCOVERY_STREAM_SPOCS_PLACEMENTS",
"DISCOVERY_STREAM_SPOCS_UPDATE",
"DISCOVERY_STREAM_SPOC_BLOCKED",
"DISCOVERY_STREAM_SPOC_IMPRESSION",
Expand Down
33 changes: 28 additions & 5 deletions common/Reducers.jsm
Expand Up @@ -68,6 +68,7 @@ const INITIAL_STATE = {
loaded: false,
frequency_caps: [],
blocked: [],
placements: [],
},
},
Search: {
Expand Down Expand Up @@ -521,15 +522,27 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
const isNotReady = () =>
!action.data || !prevState.spocs.loaded || !prevState.feeds.loaded;

const handlePlacements = handleSites => {
const { data, placements } = prevState.spocs;
const result = {};

placements.forEach(placement => {
const placementSpocs = data[placement.name];

if (!placementSpocs || !placementSpocs.length) {
return;
}

result[placement.name] = handleSites(placementSpocs);
});
return result;
};

const nextState = handleSites => ({
...prevState,
spocs: {
...prevState.spocs,
data: prevState.spocs.data.spocs
? {
spocs: handleSites(prevState.spocs.data.spocs),
}
: {},
data: handlePlacements(handleSites),
},
feeds: {
...prevState.feeds,
Expand Down Expand Up @@ -605,6 +618,16 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
INITIAL_STATE.DiscoveryStream.spocs.spocs_per_domain,
},
};
case at.DISCOVERY_STREAM_SPOCS_PLACEMENTS:
return {
...prevState,
spocs: {
...prevState.spocs,
placements:
action.data.placements ||
INITIAL_STATE.DiscoveryStream.spocs.placements,
},
};
case at.DISCOVERY_STREAM_SPOCS_UPDATE:
if (action.data) {
return {
Expand Down
65 changes: 56 additions & 9 deletions content-src/components/DiscoveryStreamBase/DiscoveryStreamBase.jsx
Expand Up @@ -6,6 +6,7 @@ import { actionCreators as ac } from "common/Actions.jsm";
import { CardGrid } from "content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid";
import { CollapsibleSection } from "content-src/components/CollapsibleSection/CollapsibleSection";
import { connect } from "react-redux";
import { DSDismiss } from "content-src/components/DiscoveryStreamComponents/DSDismiss/DSDismiss";
import { DSMessage } from "content-src/components/DiscoveryStreamComponents/DSMessage/DSMessage";
import { DSTextPromo } from "content-src/components/DiscoveryStreamComponents/DSTextPromo/DSTextPromo";
import { Hero } from "content-src/components/DiscoveryStreamComponents/Hero/Hero";
Expand Down Expand Up @@ -114,23 +115,69 @@ export class _DiscoveryStreamBase extends React.PureComponent {
case "Highlights":
return <Highlights />;
case "TopSites":
let promoAlignment;
if (
component.spocs &&
component.spocs.positions &&
component.spocs.positions.length
) {
promoAlignment =
component.spocs.positions[0].index === 0 ? "left" : "right";
}
return (
<TopSites
header={component.header}
data={component.data}
promoAlignment={component.promo_alignment}
promoAlignment={promoAlignment}
/>
);
case "TextPromo":
if (
!component.data ||
!component.data.spocs ||
!component.data.spocs[0]
) {
return null;
}
// Grab the first item in the array as we only have 1 spoc position.
const [spoc] = component.data.spocs;
const {
image_src,
alt_text,
title,
url,
context,
cta,
campaign_id,
id,
shim,
} = spoc;

return (
<DSTextPromo
image={component.properties.image_src}
alt_text={component.properties.alt_text}
header={component.properties.excerpt}
cta_text={component.properties.cta_text}
cta_url={component.properties.cta_url}
subtitle={component.properties.context}
/>
<DSDismiss
data={{
url: spoc.url,
guid: spoc.id,
shim: spoc.shim,
}}
dispatch={this.props.dispatch}
shouldSendImpressionStats={true}
>
<DSTextPromo
dispatch={this.props.dispatch}
image={image_src}
alt_text={alt_text || title}
header={title}
cta_text={cta}
cta_url={url}
subtitle={context}
campaignId={campaign_id}
id={id}
pos={0}
shim={shim}
type={component.type}
/>
</DSDismiss>
);
case "Message":
return (
Expand Down
@@ -0,0 +1,78 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

import { actionCreators as ac } from "common/Actions.jsm";
import React from "react";
import { LinkMenuOptions } from "content-src/lib/link-menu-options";

export class DSDismiss extends React.PureComponent {
constructor(props) {
super(props);
this.onDismissClick = this.onDismissClick.bind(this);
this.onHover = this.onHover.bind(this);
this.offHover = this.offHover.bind(this);
this.state = {
hovering: false,
};
}

onDismissClick() {
const index = 0;
const source = "DISCOVERY_STREAM";
const blockUrlOption = LinkMenuOptions.BlockUrl(
this.props.data,
index,
source
);

const { action, impression, userEvent } = blockUrlOption;

this.props.dispatch(action);
const userEventData = Object.assign(
{
event: userEvent,
source,
action_position: index,
},
this.props.data
);
this.props.dispatch(ac.UserEvent(userEventData));
if (impression && this.props.shouldSendImpressionStats) {
this.props.dispatch(impression);
}
}

onHover() {
this.setState({
hovering: true,
});
}

offHover() {
this.setState({
hovering: false,
});
}

render() {
let className = "ds-dismiss";
if (this.state.hovering) {
className += " hovering";
}
return (
<div className={className}>
{this.props.children}
<button
className="ds-dismiss-button"
onHover={this.onHover}
onClick={this.onDismissClick}
onMouseEnter={this.onHover}
onMouseLeave={this.offHover}
>
<span className="icon icon-dismiss" />
</button>
</div>
);
}
}
@@ -0,0 +1,38 @@
.ds-dismiss {
position: relative;
overflow: hidden;
border-radius: 8px;
transition-delay: 100ms;
transition-duration: 100ms;
transition-property: background;

&.hovering {
background: var(--newtab-element-hover-color);
}

&:hover {
.ds-dismiss-button {
opacity: 1;
}
}

.ds-dismiss-button {
border: 0;
cursor: pointer;
height: 32px;
width: 32px;
padding: 0;
display: flex;
align-items: center;
justify-content: center;
position: absolute;
right: 0;
top: 0;
border-radius: 50%;
margin: 16px 24px 0 0;
transition-duration: 200ms;
transition-property: opacity;
opacity: 0;
background: var(--newtab-element-active-color);
}
}
Expand Up @@ -2,23 +2,75 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */

import { actionCreators as ac } from "common/Actions.jsm";
import { ImpressionStats } from "../../DiscoveryStreamImpressionStats/ImpressionStats";
import React from "react";
import { SafeAnchor } from "../SafeAnchor/SafeAnchor";

export class DSTextPromo extends React.PureComponent {
constructor(props) {
super(props);
this.onLinkClick = this.onLinkClick.bind(this);
}

onLinkClick() {
if (this.props.dispatch) {
this.props.dispatch(
ac.UserEvent({
event: "CLICK",
source: this.props.type.toUpperCase(),
action_position: this.props.pos,
})
);

this.props.dispatch(
ac.ImpressionStats({
source: this.props.type.toUpperCase(),
click: 0,
tiles: [
{
id: this.props.id,
pos: this.props.pos,
...(this.props.shim && this.props.shim.click
? { shim: this.props.shim.click }
: {}),
},
],
})
);
}
}

render() {
return (
<div className="ds-text-promo">
<img src={this.props.image} alt={this.props.alt_text} />
<div className="text">
<h3>
{`${this.props.header}\u2003`}
<SafeAnchor className="ds-chevron-link" url={this.props.cta_url}>
<SafeAnchor
className="ds-chevron-link"
dispatch={this.props.dispatch}
onLinkClick={this.onLinkClick}
url={this.props.cta_url}
>
{this.props.cta_text}
</SafeAnchor>
</h3>
<p className="subtitle">{this.props.subtitle}</p>
</div>
<ImpressionStats
campaignId={this.props.campaignId}
rows={[
{
id: this.props.id,
pos: this.props.pos,
shim: this.props.shim && this.props.shim.impression,
},
]}
dispatch={this.props.dispatch}
source={this.props.type}
/>
</div>
);
}
Expand Down
Expand Up @@ -84,6 +84,15 @@ export class _TopSites extends React.PureComponent {
label: topSiteSpoc.sponsor,
title: topSiteSpoc.sponsor,
url: topSiteSpoc.url,
campaignId: topSiteSpoc.campaign_id,
id: topSiteSpoc.id,
guid: topSiteSpoc.id,
shim: topSiteSpoc.shim,
// For now we are assuming position based on intended position.
// Actual position can shift based on other content.
// We also hard code left and right to be 0 and 7.
// We send the intended postion in the ping.
pos: promoAlignment === "left" ? 0 : 7,
};

const firstAvailableIndex = this.getFirstAvailableIndex(
Expand Down

0 comments on commit 5fb6a1a

Please sign in to comment.