Bug 1574334 - Add lazy cards, story engagements and bug fixes to New Tab Page r=pdahiya,fluent-reviewers,flod

Differential Revision: https://phabricator.services.mozilla.com/D42229

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ed Lee 2019-08-16 05:54:16 +00:00
Родитель 3aec893f88
Коммит 5ff7943b7d
29 изменённых файлов: 483 добавлений и 125 удалений

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

@ -38,6 +38,10 @@
&:active {
background-color: $grey-90-30;
}
&:focus {
box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;
}
}
}

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

@ -0,0 +1,141 @@
# User Actions
A subset of actions are available to messages via fields like `button_action` for snippets, or `primary_action` for CFRs.
## Usage
For snippets, you should add the action type in `button_action` and any additional parameters in `button_action_args. For example:
```json
{
"button_action": "OPEN_ABOUT_PAGE",
"button_action_args": "config"
}
```
## Available Actions
### `OPEN_APPLICATIONS_MENU`
* args: (none)
Opens the applications menu.
### `OPEN_PRIVATE_BROWSER_WINDOW`
* args: (none)
Opens a new private browsing window.
### `OPEN_URL`
* args: `string` (a url)
Opens a given url.
Example:
```json
{
"button_action": "OPEN_URL",
"button_action_args": "https://foo.com"
}
```
### `OPEN_ABOUT_PAGE`
* args: `string` (a valid about page without the `about:` prefix)
Opens a given about page
Example:
```json
{
"button_action": "OPEN_ABOUT_PAGE",
"button_action_args": "config"
}
```
### `OPEN_PREFERENCES_PAGE`
* args: `string` (a category accessible via a `#`)
Opens `about:preferences` with an optional category accessible via a `#` in the URL (e.g. `about:preferences#home`).
Example:
```json
{
"button_action": "OPEN_PREFERENCES_PAGE",
"button_action_args": "home"
}
```
### `SHOW_FIREFOX_ACCOUNTS`
* args: (none)
Opens Firefox accounts sign-up page. Encodes some information that the origin was from snippets by default.
### `PIN_CURRENT_TAB`
* args: (none)
Pins the currently focused tab.
### `ENABLE_FIREFOX_MONITOR`
* args:
```ts
{
url: string;
flowRequestParams: {
entrypoint: string;
utm_term: string;
form_type: string;
}
}
```
Opens an oauth flow to enable Firefox Monitor at a given `url` and adds Firefox metrics that user given a set of `flowRequestParams`.
### `url`
The URL should start with `https://monitor.firefox.com/oauth/init` and add various metrics tags as search params, including:
* `utm_source`
* `utm_campaign`
* `form_type`
* `entrypoint`
You should verify the values of these search params with whoever is doing the data analysis (e.g. Leif Oines).
### `flowRequestParams`
These params are used by Firefox to add information specific to that individual user to the final oauth URL. You should include:
* `entrypoint`
* `utm_term`
* `form_type`
The `entrypoint` and `form_type` values should match the encoded values in your `url`.
You should verify the values with whoever is doing the data analysis (e.g. Leif Oines).
### Example
```json
{
"button_action": "ENABLE_FIREFOX_MONITOR",
"button_action_args": {
"url": "https://monitor.firefox.com/oauth/init?utm_source=snippets&utm_campaign=monitor-snippet-test&form_type=email&entrypoint=newtab",
"flowRequestParams": {
"entrypoint": "snippets",
"utm_term": "monitor",
"form_type": "email"
}
}
}
```

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

@ -29,6 +29,7 @@
.blockButton {
display: block;
opacity: 1;
// larger inset if discovery stream is enabled.
.ds-outer-wrapper-breakpoint-override & {
@ -105,6 +106,11 @@
inset-inline-end: 20px;
opacity: 1;
top: 50%;
&:focus {
box-shadow: 0 0 0 1px $blue-50 inset, 0 0 0 1px $blue-50, 0 0 0 4px $blue-50-30;
border-radius: 2px;
}
}
.title {
@ -149,12 +155,17 @@
background-color: transparent;
.blockButton {
display: none;
display: block;
inset-inline-end: -15%;
opacity: 1;
opacity: 0;
margin: auto;
top: unset;
&:focus {
opacity: 1;
box-shadow: none;
}
@media (max-width: 1120px) {
inset-inline-end: 2%;
}

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

@ -36,6 +36,7 @@ export class CardGrid extends React.PureComponent {
pocket_id={rec.pocket_id}
context_type={rec.context_type}
bookmarkGuid={rec.bookmarkGuid}
engagement={rec.engagement}
cta={rec.cta}
cta_variant={this.props.cta_variant}
/>

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

@ -17,6 +17,7 @@ export const DefaultMeta = ({
context,
context_type,
cta,
engagement,
}) => (
<div className="meta">
<div className="info-wrap">
@ -29,7 +30,11 @@ export const DefaultMeta = ({
</div>
)}
</div>
<DSContextFooter context_type={context_type} context={context} />
<DSContextFooter
context_type={context_type}
context={context}
engagement={engagement}
/>
</div>
);
@ -40,6 +45,7 @@ export const VariantMeta = ({
context,
context_type,
cta,
engagement,
sponsor,
}) => (
<div className="meta">
@ -51,9 +57,13 @@ export const VariantMeta = ({
<header className="title clamp">{title}</header>
{excerpt && <p className="excerpt clamp">{excerpt}</p>}
</div>
{cta && <button className="button cta-button">{cta}</button>}
{context && cta && <button className="button cta-button">{cta}</button>}
{!context && (
<DSContextFooter context_type={context_type} context={context} />
<DSContextFooter
context_type={context_type}
context={context}
engagement={engagement}
/>
)}
</div>
);
@ -63,6 +73,13 @@ export class DSCard extends React.PureComponent {
super(props);
this.onLinkClick = this.onLinkClick.bind(this);
this.setPlaceholderRef = element => {
this.placholderElement = element;
};
this.state = {
isSeen: false,
};
}
onLinkClick(event) {
@ -93,9 +110,45 @@ export class DSCard extends React.PureComponent {
}
}
onSeen(entries) {
if (this.state) {
const entry = entries.find(e => e.isIntersecting);
if (entry) {
if (this.placholderElement) {
this.observer.unobserve(this.placholderElement);
}
// Stop observing since element has been seen
this.setState({
isSeen: true,
});
}
}
}
componentDidMount() {
if (this.placholderElement) {
this.observer = new IntersectionObserver(this.onSeen.bind(this));
this.observer.observe(this.placholderElement);
}
}
componentWillUnmount() {
// Remove observer on unmount
if (this.observer && this.placholderElement) {
this.observer.unobserve(this.placholderElement);
}
}
render() {
if (this.props.placeholder || !this.state.isSeen) {
return (
<div className="ds-card placeholder" ref={this.setPlaceholderRef} />
);
}
return (
<div className={`ds-card${this.props.placeholder ? " placeholder" : ""}`}>
<div className="ds-card">
<SafeAnchor
className="ds-card-link"
dispatch={this.props.dispatch}
@ -116,6 +169,7 @@ export class DSCard extends React.PureComponent {
excerpt={this.props.excerpt}
context={this.props.context}
context_type={this.props.context_type}
engagement={this.props.engagement}
cta={this.props.cta}
sponsor={this.props.sponsor}
/>
@ -126,6 +180,7 @@ export class DSCard extends React.PureComponent {
title={this.props.title}
excerpt={this.props.excerpt}
context={this.props.context}
engagement={this.props.engagement}
context_type={this.props.context_type}
cta={this.props.cta}
/>
@ -145,20 +200,18 @@ export class DSCard extends React.PureComponent {
source={this.props.type}
/>
</SafeAnchor>
{!this.props.placeholder && (
<DSLinkMenu
id={this.props.id}
index={this.props.pos}
dispatch={this.props.dispatch}
url={this.props.url}
title={this.props.title}
source={this.props.source}
type={this.props.type}
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
/>
)}
<DSLinkMenu
id={this.props.id}
index={this.props.pos}
dispatch={this.props.dispatch}
url={this.props.url}
title={this.props.title}
source={this.props.source}
type={this.props.type}
pocket_id={this.props.pocket_id}
shim={this.props.shim}
bookmarkGuid={this.props.bookmarkGuid}
/>
</div>
);
}

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

@ -13,14 +13,7 @@ $excerpt-line-height: 20;
background: transparent;
box-shadow: inset $inner-box-shadow;
border-radius: 4px;
.ds-card-link {
cursor: default;
}
.img-wrapper {
opacity: 0;
}
min-height: 300px;
}
.img-wrapper {

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

@ -21,20 +21,24 @@ export const StatusMessage = ({ icon, fluentID }) => (
export class DSContextFooter extends React.PureComponent {
render() {
const { context, context_type } = this.props;
const { context, context_type, engagement } = this.props;
const { icon, fluentID } = cardContextTypes[context_type] || {};
return (
<div className="story-footer">
{context && <p className="story-sponsored-label clamp">{context}</p>}
<TransitionGroup component={null}>
{!context && context_type && (
{!context && (context_type || engagement) && (
<CSSTransition
key={fluentID}
timeout={ANIMATION_DURATION}
classNames="story-animate"
>
<StatusMessage icon={icon} fluentID={fluentID} />
{engagement && !context_type ? (
<div className="story-view-count">{engagement}</div>
) : (
<StatusMessage icon={icon} fluentID={fluentID} />
)}
</CSSTransition>
)}
</TransitionGroup>

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

@ -8,6 +8,7 @@ $status-dark-green: #7C6;
position: relative;
.story-sponsored-label,
.story-view-count,
.status-message {
@include dark-theme-only {
color: $grey-40;

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

@ -78,6 +78,7 @@ export class Hero extends React.PureComponent {
source={rec.domain}
pocket_id={rec.pocket_id}
bookmarkGuid={rec.bookmarkGuid}
engagement={rec.engagement}
/>
)
);
@ -112,6 +113,7 @@ export class Hero extends React.PureComponent {
<DSContextFooter
context={heroRec.context}
context_type={heroRec.context_type}
engagement={heroRec.engagement}
/>
</div>
<ImpressionStats

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

@ -45,6 +45,7 @@ $card-header-in-hero-line-height: 20;
.ds-card.placeholder {
margin-bottom: 20px;
padding-bottom: 20px;
min-height: 180px;
}
.img-wrapper {

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

@ -81,6 +81,7 @@ export class ListItem extends React.PureComponent {
<DSContextFooter
context={this.props.context}
context_type={this.props.context_type}
engagement={this.props.engagement}
/>
</div>
<DSImage
@ -159,6 +160,7 @@ export function _List(props) {
url={rec.url}
pocket_id={rec.pocket_id}
bookmarkGuid={rec.bookmarkGuid}
engagement={rec.engagement}
/>
)
);

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

@ -1991,7 +1991,8 @@ main {
margin: 0 0 12px; }
.ds-hero .ds-card.placeholder {
margin-bottom: 20px;
padding-bottom: 20px; }
padding-bottom: 20px;
min-height: 180px; }
.ds-hero .img-wrapper {
margin: 0 0 12px; }
.ds-hero .ds-hero-item {
@ -2635,11 +2636,8 @@ main {
.ds-card.placeholder {
background: transparent;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
border-radius: 4px; }
.ds-card.placeholder .ds-card-link {
cursor: default; }
.ds-card.placeholder .img-wrapper {
opacity: 0; }
border-radius: 4px;
min-height: 300px; }
.ds-card .img-wrapper {
width: 100%; }
.ds-card .img {
@ -2783,12 +2781,14 @@ main {
margin-top: 12px;
position: relative; }
.story-footer .story-sponsored-label,
.story-footer .story-view-count,
.story-footer .status-message {
-webkit-line-clamp: 1;
font-size: 13px;
line-height: 24px;
color: #737373; }
[lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
.story-footer .story-view-count, [lwt-newtab-brighttext]
.story-footer .status-message {
color: #B1B1B3; }
.story-footer .status-message {
@ -3003,6 +3003,8 @@ main {
background-color: rgba(12, 12, 13, 0.2); }
.ASRouterButton.secondary:active {
background-color: rgba(12, 12, 13, 0.3); }
.ASRouterButton.secondary:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
[lwt-newtab-brighttext] .secondary {
background-color: rgba(249, 249, 250, 0.1); }
@ -3313,7 +3315,8 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color); }
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block; }
display: block;
opacity: 1; }
.ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
inset-inline-end: -8%; }
@media (max-width: 865px) {
@ -3371,6 +3374,9 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
inset-inline-end: 20px;
opacity: 1;
top: 50%; }
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
border-radius: 2px; }
.SimpleBelowSearchSnippet .title {
font-size: inherit;
margin: 0; }
@ -3397,11 +3403,14 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-height: 60px;
background-color: transparent; }
.SimpleBelowSearchSnippet.withButton .blockButton {
display: none;
display: block;
inset-inline-end: -15%;
opacity: 1;
opacity: 0;
margin: auto;
top: unset; }
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none; }
@media (max-width: 1120px) {
.SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%; } }

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

@ -1994,7 +1994,8 @@ main {
margin: 0 0 12px; }
.ds-hero .ds-card.placeholder {
margin-bottom: 20px;
padding-bottom: 20px; }
padding-bottom: 20px;
min-height: 180px; }
.ds-hero .img-wrapper {
margin: 0 0 12px; }
.ds-hero .ds-hero-item {
@ -2638,11 +2639,8 @@ main {
.ds-card.placeholder {
background: transparent;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
border-radius: 4px; }
.ds-card.placeholder .ds-card-link {
cursor: default; }
.ds-card.placeholder .img-wrapper {
opacity: 0; }
border-radius: 4px;
min-height: 300px; }
.ds-card .img-wrapper {
width: 100%; }
.ds-card .img {
@ -2786,12 +2784,14 @@ main {
margin-top: 12px;
position: relative; }
.story-footer .story-sponsored-label,
.story-footer .story-view-count,
.story-footer .status-message {
-webkit-line-clamp: 1;
font-size: 13px;
line-height: 24px;
color: #737373; }
[lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
.story-footer .story-view-count, [lwt-newtab-brighttext]
.story-footer .status-message {
color: #B1B1B3; }
.story-footer .status-message {
@ -3006,6 +3006,8 @@ main {
background-color: rgba(12, 12, 13, 0.2); }
.ASRouterButton.secondary:active {
background-color: rgba(12, 12, 13, 0.3); }
.ASRouterButton.secondary:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
[lwt-newtab-brighttext] .secondary {
background-color: rgba(249, 249, 250, 0.1); }
@ -3316,7 +3318,8 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color); }
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block; }
display: block;
opacity: 1; }
.ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
inset-inline-end: -8%; }
@media (max-width: 865px) {
@ -3374,6 +3377,9 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
inset-inline-end: 20px;
opacity: 1;
top: 50%; }
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
border-radius: 2px; }
.SimpleBelowSearchSnippet .title {
font-size: inherit;
margin: 0; }
@ -3400,11 +3406,14 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-height: 60px;
background-color: transparent; }
.SimpleBelowSearchSnippet.withButton .blockButton {
display: none;
display: block;
inset-inline-end: -15%;
opacity: 1;
opacity: 0;
margin: auto;
top: unset; }
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none; }
@media (max-width: 1120px) {
.SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%; } }

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

@ -1991,7 +1991,8 @@ main {
margin: 0 0 12px; }
.ds-hero .ds-card.placeholder {
margin-bottom: 20px;
padding-bottom: 20px; }
padding-bottom: 20px;
min-height: 180px; }
.ds-hero .img-wrapper {
margin: 0 0 12px; }
.ds-hero .ds-hero-item {
@ -2635,11 +2636,8 @@ main {
.ds-card.placeholder {
background: transparent;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color);
border-radius: 4px; }
.ds-card.placeholder .ds-card-link {
cursor: default; }
.ds-card.placeholder .img-wrapper {
opacity: 0; }
border-radius: 4px;
min-height: 300px; }
.ds-card .img-wrapper {
width: 100%; }
.ds-card .img {
@ -2783,12 +2781,14 @@ main {
margin-top: 12px;
position: relative; }
.story-footer .story-sponsored-label,
.story-footer .story-view-count,
.story-footer .status-message {
-webkit-line-clamp: 1;
font-size: 13px;
line-height: 24px;
color: #737373; }
[lwt-newtab-brighttext] .story-footer .story-sponsored-label, [lwt-newtab-brighttext]
.story-footer .story-view-count, [lwt-newtab-brighttext]
.story-footer .status-message {
color: #B1B1B3; }
.story-footer .status-message {
@ -3003,6 +3003,8 @@ main {
background-color: rgba(12, 12, 13, 0.2); }
.ASRouterButton.secondary:active {
background-color: rgba(12, 12, 13, 0.3); }
.ASRouterButton.secondary:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3); }
[lwt-newtab-brighttext] .secondary {
background-color: rgba(249, 249, 250, 0.1); }
@ -3313,7 +3315,8 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
.below-search-snippet.withButton .snippet-hover-wrapper:hover {
background-color: var(--newtab-element-hover-color); }
.below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
display: block; }
display: block;
opacity: 1; }
.ds-outer-wrapper-breakpoint-override .below-search-snippet.withButton .snippet-hover-wrapper:hover .blockButton {
inset-inline-end: -8%; }
@media (max-width: 865px) {
@ -3371,6 +3374,9 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
inset-inline-end: 20px;
opacity: 1;
top: 50%; }
.SimpleBelowSearchSnippet .blockButton:focus {
box-shadow: 0 0 0 1px #0A84FF inset, 0 0 0 1px #0A84FF, 0 0 0 4px rgba(10, 132, 255, 0.3);
border-radius: 2px; }
.SimpleBelowSearchSnippet .title {
font-size: inherit;
margin: 0; }
@ -3397,11 +3403,14 @@ body[lwt-newtab-brighttext] .scene2Icon .icon-light-theme {
min-height: 60px;
background-color: transparent; }
.SimpleBelowSearchSnippet.withButton .blockButton {
display: none;
display: block;
inset-inline-end: -15%;
opacity: 1;
opacity: 0;
margin: auto;
top: unset; }
.SimpleBelowSearchSnippet.withButton .blockButton:focus {
opacity: 1;
box-shadow: none; }
@media (max-width: 1120px) {
.SimpleBelowSearchSnippet.withButton .blockButton {
inset-inline-end: 2%; } }

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

@ -7895,7 +7895,8 @@ class DSContextFooter_DSContextFooter extends external_React_default.a.PureCompo
render() {
const {
context,
context_type
context_type,
engagement
} = this.props;
const {
icon,
@ -7907,11 +7908,13 @@ class DSContextFooter_DSContextFooter extends external_React_default.a.PureCompo
className: "story-sponsored-label clamp"
}, context), external_React_default.a.createElement(external_ReactTransitionGroup_["TransitionGroup"], {
component: null
}, !context && context_type && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], {
}, !context && (context_type || engagement) && external_React_default.a.createElement(external_ReactTransitionGroup_["CSSTransition"], {
key: fluentID,
timeout: ANIMATION_DURATION,
classNames: "story-animate"
}, external_React_default.a.createElement(StatusMessage, {
}, engagement && !context_type ? external_React_default.a.createElement("div", {
className: "story-view-count"
}, engagement) : external_React_default.a.createElement(StatusMessage, {
icon: icon,
fluentID: fluentID
}))));
@ -7935,7 +7938,8 @@ const DefaultMeta = ({
excerpt,
context,
context_type,
cta
cta,
engagement
}) => external_React_default.a.createElement("div", {
className: "meta"
}, external_React_default.a.createElement("div", {
@ -7952,7 +7956,8 @@ const DefaultMeta = ({
tabIndex: "0"
}, cta)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context_type: context_type,
context: context
context: context,
engagement: engagement
}));
const VariantMeta = ({
source,
@ -7961,6 +7966,7 @@ const VariantMeta = ({
context,
context_type,
cta,
engagement,
sponsor
}) => external_React_default.a.createElement("div", {
className: "meta"
@ -7972,16 +7978,25 @@ const VariantMeta = ({
className: "title clamp"
}, title), excerpt && external_React_default.a.createElement("p", {
className: "excerpt clamp"
}, excerpt)), cta && external_React_default.a.createElement("button", {
}, excerpt)), context && cta && external_React_default.a.createElement("button", {
className: "button cta-button"
}, cta), !context && external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context_type: context_type,
context: context
context: context,
engagement: engagement
}));
class DSCard_DSCard extends external_React_default.a.PureComponent {
constructor(props) {
super(props);
this.onLinkClick = this.onLinkClick.bind(this);
this.setPlaceholderRef = element => {
this.placholderElement = element;
};
this.state = {
isSeen: false
};
}
onLinkClick(event) {
@ -8005,9 +8020,47 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
}
}
onSeen(entries) {
if (this.state) {
const entry = entries.find(e => e.isIntersecting);
if (entry) {
if (this.placholderElement) {
this.observer.unobserve(this.placholderElement);
} // Stop observing since element has been seen
this.setState({
isSeen: true
});
}
}
}
componentDidMount() {
if (this.placholderElement) {
this.observer = new IntersectionObserver(this.onSeen.bind(this));
this.observer.observe(this.placholderElement);
}
}
componentWillUnmount() {
// Remove observer on unmount
if (this.observer && this.placholderElement) {
this.observer.unobserve(this.placholderElement);
}
}
render() {
if (this.props.placeholder || !this.state.isSeen) {
return external_React_default.a.createElement("div", {
className: "ds-card placeholder",
ref: this.setPlaceholderRef
});
}
return external_React_default.a.createElement("div", {
className: `ds-card${this.props.placeholder ? " placeholder" : ""}`
className: "ds-card"
}, external_React_default.a.createElement(SafeAnchor_SafeAnchor, {
className: "ds-card-link",
dispatch: this.props.dispatch,
@ -8025,6 +8078,7 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
excerpt: this.props.excerpt,
context: this.props.context,
context_type: this.props.context_type,
engagement: this.props.engagement,
cta: this.props.cta,
sponsor: this.props.sponsor
}), !this.props.cta_variant && external_React_default.a.createElement(DefaultMeta, {
@ -8032,6 +8086,7 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
title: this.props.title,
excerpt: this.props.excerpt,
context: this.props.context,
engagement: this.props.engagement,
context_type: this.props.context_type,
cta: this.props.cta
}), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
@ -8045,7 +8100,7 @@ class DSCard_DSCard extends external_React_default.a.PureComponent {
}],
dispatch: this.props.dispatch,
source: this.props.type
})), !this.props.placeholder && external_React_default.a.createElement(DSLinkMenu_DSLinkMenu, {
})), external_React_default.a.createElement(DSLinkMenu_DSLinkMenu, {
id: this.props.id,
index: this.props.pos,
dispatch: this.props.dispatch,
@ -8192,6 +8247,7 @@ class CardGrid_CardGrid extends external_React_default.a.PureComponent {
pocket_id: rec.pocket_id,
context_type: rec.context_type,
bookmarkGuid: rec.bookmarkGuid,
engagement: rec.engagement,
cta: rec.cta,
cta_variant: this.props.cta_variant
}));
@ -8336,7 +8392,8 @@ class List_ListItem extends external_React_default.a.PureComponent {
className: "ds-list-item-excerpt clamp"
}, this.props.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context: this.props.context,
context_type: this.props.context_type
context_type: this.props.context_type,
engagement: this.props.engagement
})), external_React_default.a.createElement(DSImage_DSImage, {
extraClassNames: "ds-list-image",
source: this.props.image_src,
@ -8400,7 +8457,8 @@ function _List(props) {
type: props.type,
url: rec.url,
pocket_id: rec.pocket_id,
bookmarkGuid: rec.bookmarkGuid
bookmarkGuid: rec.bookmarkGuid,
engagement: rec.engagement
}));
}
@ -8513,7 +8571,8 @@ class Hero_Hero extends external_React_default.a.PureComponent {
context_type: rec.context_type,
source: rec.domain,
pocket_id: rec.pocket_id,
bookmarkGuid: rec.bookmarkGuid
bookmarkGuid: rec.bookmarkGuid,
engagement: rec.engagement
}));
}
@ -8548,7 +8607,8 @@ class Hero_Hero extends external_React_default.a.PureComponent {
className: "excerpt clamp"
}, heroRec.excerpt)), external_React_default.a.createElement(DSContextFooter_DSContextFooter, {
context: heroRec.context,
context_type: heroRec.context_type
context_type: heroRec.context_type,
engagement: heroRec.engagement
})), external_React_default.a.createElement(ImpressionStats["ImpressionStats"], {
campaignId: heroRec.campaign_id,
rows: [{

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

@ -399,10 +399,9 @@ const ONBOARDING_MESSAGES = () => [
id: "PROTECTIONS_PANEL_1",
template: "protections_panel",
content: {
title: "Browse without being followed",
body:
"Keep your data to yourself. Firefox protects you from many of the most common trackers that follow what you do online.",
link_text: "Learn more",
title: { string_id: "cfr-protections-panel-header" },
body: { string_id: "cfr-protections-panel-body" },
link_text: { string_id: "cfr-protections-panel-link-text" },
cta_url: `${Services.urlFormatter.formatURLPref(
"app.support.baseURL"
)}etp-promotions?as=u&utm_source=inproduct`,

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

@ -48,6 +48,11 @@ ChromeUtils.defineModuleGetter(
"clearInterval",
"resource://gre/modules/Timer.jsm"
);
ChromeUtils.defineModuleGetter(
this,
"requestIdleCallback",
"resource://gre/modules/Timer.jsm"
);
// Frequency at which to check for new messages
const SYSTEM_TICK_INTERVAL = 5 * 60 * 1000;
@ -257,17 +262,17 @@ class _ToolbarBadgeHub {
}
registerBadgeToAllWindows(message) {
// Impression should be added when the badge becomes visible
this._addImpression(message);
// Send a telemetry ping when adding the notification badge
this.sendUserEventTelemetry("IMPRESSION", message);
if (message.template === "update_action") {
this.executeAction({ ...message.content.action, message_id: message.id });
// No badge to set only an action to execute
return;
}
// Impression should be added when the badge becomes visible
this._addImpression(message);
// Send a telemetry ping when adding the notification badge
this.sendUserEventTelemetry("IMPRESSION", message);
EveryWindow.registerCallback(
this.id,
win => {
@ -297,7 +302,7 @@ class _ToolbarBadgeHub {
if (message.content.delay) {
this.state.showBadgeTimeoutId = setTimeout(() => {
this.registerBadgeToAllWindows(message);
requestIdleCallback(() => this.registerBadgeToAllWindows(message));
}, message.content.delay);
} else {
this.registerBadgeToAllWindows(message);

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

@ -378,6 +378,8 @@ class _ToolbarPanelHub {
*/
async insertProtectionPanelMessage(event) {
const win = event.target.ownerGlobal;
this.maybeInsertFTL(win);
const doc = event.target.ownerDocument;
const container = doc.getElementById("messaging-system-message-container");
const infoButton = doc.getElementById("protections-popup-info-button");

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

@ -78,6 +78,12 @@ cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
.aria-label = Close button
.title = Close
## Protections panel
cfr-protections-panel-header = Browse without being followed
cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel
cfr-whatsnew-button =

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

@ -11,7 +11,6 @@
## avoid breaking quoted text).
onboarding-button-label-learn-more = Learn More
onboarding-button-label-try-now = Try It Now
onboarding-button-label-get-started = Get Started
## Welcome modal dialog strings
@ -76,21 +75,6 @@ onboarding-benefit-privacy-text = Everything we do honors our Personal Data Prom
## Each message has a title and a description of what the browser feature is.
## Each message also has an associated button for the user to try the feature.
## The string for the button is found above, in the UI strings section
onboarding-private-browsing-title = Private Browsing
onboarding-private-browsing-text = Browse by yourself. Private Browsing with Content Blocking blocks online trackers that follow you around the web.
onboarding-screenshots-title = Screenshots
onboarding-screenshots-text = Take, save and share screenshots - without leaving { -brand-short-name }. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.
onboarding-addons-title = Add-ons
onboarding-addons-text = Add even more features that make { -brand-short-name } work harder for you. Compare prices, check the weather or express your personality with a custom theme.
onboarding-ghostery-title = Ghostery
onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
# Note: "Sync" in this case is a generic verb, as in "to synchronize"
onboarding-fxa-title = Sync
onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
onboarding-tracking-protection-title2 = Protection From Tracking
onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web.

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

@ -16,7 +16,7 @@ const FAKE_TRIPLETS = [
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
label: { string_id: "onboarding-button-label-get-started" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },

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

@ -22,7 +22,7 @@ const L10N_CONTENT = {
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
label: { string_id: "onboarding-button-label-get-started" },
action: { type: "SOME_TYPE" },
},
};

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

@ -11,7 +11,7 @@ export const CARDS = [
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
label: { string_id: "onboarding-button-label-get-started" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },

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

@ -11,7 +11,7 @@ const CARDS = [
text: { string_id: "onboarding-private-browsing-text" },
icon: "icon",
primary_button: {
label: { string_id: "onboarding-button-label-try-now" },
label: { string_id: "onboarding-button-label-get-started" },
action: {
type: "OPEN_URL",
data: { args: "https://example.com/" },

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

@ -20,6 +20,7 @@ describe("<DSCard>", () => {
beforeEach(() => {
wrapper = shallow(<DSCard />);
wrapper.setState({ isSeen: true });
sandbox = sinon.createSandbox();
});
@ -79,6 +80,7 @@ describe("<DSCard>", () => {
it("should render badges for pocket, bookmark when not a spoc element ", () => {
wrapper = mount(<DSCard context_type="bookmark" />);
wrapper.setState({ isSeen: true });
const contextFooter = wrapper.find(DSContextFooter);
assert.lengthOf(contextFooter.find(StatusMessage), 1);
@ -87,6 +89,7 @@ describe("<DSCard>", () => {
it("should render Sponsored Context for a spoc element", () => {
const context = "Sponsored by Foo";
wrapper = mount(<DSCard context_type="bookmark" context={context} />);
wrapper.setState({ isSeen: true });
const contextFooter = wrapper.find(DSContextFooter);
assert.lengthOf(contextFooter.find(StatusMessage), 0);
@ -99,6 +102,7 @@ describe("<DSCard>", () => {
beforeEach(() => {
dispatch = sandbox.stub();
wrapper = shallow(<DSCard dispatch={dispatch} />);
wrapper.setState({ isSeen: true });
});
it("should call dispatch with the correct events", () => {
@ -160,6 +164,7 @@ describe("<DSCard>", () => {
describe("DSCard with CTA", () => {
beforeEach(() => {
wrapper = mount(<DSCard />);
wrapper.setState({ isSeen: true });
});
it("should render Default Meta", () => {
@ -178,9 +183,19 @@ describe("<DSCard>", () => {
assert.equal(meta.find(".cta-link").text(), "test");
});
it("should render cta-button when item has cta and cta button variant is true", () => {
it("should not render cta-button for non spoc content", () => {
wrapper.setProps({ cta: "test", cta_variant: true });
const meta = wrapper.find(VariantMeta);
assert.lengthOf(meta.find(".cta-button"), 0);
});
it("should render cta-button when item has cta and cta button variant is true and is spoc", () => {
wrapper.setProps({
cta: "test",
cta_variant: true,
context: "Sponsored by Foo",
});
const meta = wrapper.find(VariantMeta);
assert.equal(meta.find(".cta-button").text(), "test");
});
@ -207,6 +222,44 @@ describe("<DSCard>", () => {
assert.equal(meta.find(".source").text(), "Test · Sponsored");
});
});
describe("DSCard with Intersection Observer", () => {
beforeEach(() => {
wrapper = shallow(<DSCard />);
});
it("should render card when seen", () => {
let card = wrapper.find("div.ds-card.placeholder");
assert.lengthOf(card, 1);
wrapper.instance().observer = {
unobserve: sandbox.stub(),
};
wrapper.instance().placholderElement = "element";
wrapper.instance().onSeen([
{
isIntersecting: true,
},
]);
assert.isTrue(wrapper.instance().state.isSeen);
card = wrapper.find("div.ds-card.placeholder");
assert.lengthOf(card, 0);
assert.lengthOf(wrapper.find(SafeAnchor), 1);
assert.calledOnce(wrapper.instance().observer.unobserve);
assert.calledWith(wrapper.instance().observer.unobserve, "element");
});
it("should setup proper placholder ref for isSeen", () => {
wrapper.instance().setPlaceholderRef("element");
assert.equal(wrapper.instance().placholderElement, "element");
});
it("should setup observer on componentDidMount", () => {
wrapper = mount(<DSCard />);
assert.isTrue(!!wrapper.instance().observer);
});
});
});
describe("<PlaceholderDSCard> component", () => {
@ -221,21 +274,21 @@ describe("<PlaceholderDSCard> component", () => {
it("should contain placeholder div", () => {
const wrapper = shallow(<DSCard placeholder={true} />);
wrapper.setState({ isSeen: true });
const card = wrapper.find("div.ds-card.placeholder");
assert.lengthOf(card, 1);
});
it("should not be clickable", () => {
const wrapper = shallow(<DSCard placeholder={true} />);
wrapper.setState({ isSeen: true });
const anchor = wrapper.find("SafeAnchor.ds-card-link");
assert.lengthOf(anchor, 1);
const linkClick = anchor.prop("onLinkClick");
assert.isUndefined(linkClick);
assert.lengthOf(anchor, 0);
});
it("should not have context menu", () => {
const wrapper = shallow(<DSCard placeholder={true} />);
wrapper.setState({ isSeen: true });
const linkMenu = wrapper.find(DSLinkMenu);
assert.lengthOf(linkMenu, 0);
});

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

@ -12,6 +12,7 @@ describe("<DSContextFooter>", () => {
const bookmarkBadge = "bookmark";
const removeBookmarkBadge = "removedBookmark";
const context = "Sponsored by Babel";
const engagement = "Popular";
beforeEach(() => {
wrapper = mount(<DSContextFooter />);
@ -26,19 +27,32 @@ describe("<DSContextFooter>", () => {
assert.isTrue(wrapper.exists());
assert.isOk(wrapper.find(".story-footer"));
});
it("should render an engagement status if no badge and spoc passed", () => {
wrapper = mount(<DSContextFooter engagement={engagement} />);
const engagementLabel = wrapper.find(".story-view-count");
assert.equal(engagementLabel.text(), engagement);
});
it("should render a badge if a proper badge prop is passed", () => {
wrapper = mount(<DSContextFooter context_type={bookmarkBadge} />);
wrapper = mount(
<DSContextFooter context_type={bookmarkBadge} engagement={engagement} />
);
const { fluentID } = cardContextTypes[bookmarkBadge];
assert.lengthOf(wrapper.find(".story-view-count"), 0);
const statusLabel = wrapper.find(".story-context-label");
assert.isOk(statusLabel);
assert.equal(statusLabel.prop("data-l10n-id"), fluentID);
});
it("should only render a sponsored context if pass a sponsored context", async () => {
wrapper = mount(
<DSContextFooter context_type={bookmarkBadge} context={context} />
<DSContextFooter
context_type={bookmarkBadge}
context={context}
engagement={engagement}
/>
);
assert.lengthOf(wrapper.find(".story-view-count"), 0);
assert.lengthOf(wrapper.find(StatusMessage), 0);
assert.equal(wrapper.find(".story-sponsored-label").text(), context);
});

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

@ -23,6 +23,7 @@ describe("ToolbarBadgeHub", () => {
let getStringPrefStub;
let clearUserPrefStub;
let setStringPrefStub;
let requestIdleCallbackStub;
beforeEach(async () => {
globals = new GlobalOverrider();
sandbox = sinon.createSandbox();
@ -67,7 +68,9 @@ describe("ToolbarBadgeHub", () => {
getStringPrefStub = sandbox.stub();
clearUserPrefStub = sandbox.stub();
setStringPrefStub = sandbox.stub();
requestIdleCallbackStub = sandbox.stub().callsFake(fn => fn());
globals.set({
requestIdleCallback: requestIdleCallbackStub,
EveryWindow: everyWindowStub,
PrivateBrowsingUtils: { isBrowserPrivate: isBrowserPrivateStub },
setTimeout: setTimeoutStub,
@ -562,6 +565,8 @@ describe("ToolbarBadgeHub", () => {
instance.registerBadgeToAllWindows,
msg_with_delay
);
// Delayed actions should be executed inside requestIdleCallback
assert.calledOnce(requestIdleCallbackStub);
});
});
describe("#sendUserEventTelemetry", () => {

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

@ -78,6 +78,12 @@ cfr-doorhanger-bookmark-fxa-close-btn-tooltip =
.aria-label = Close button
.title = Close
## Protections panel
cfr-protections-panel-header = Browse without being followed
cfr-protections-panel-body = Keep your data to yourself. { -brand-short-name } protects you from many of the most common trackers that follow what you do online.
cfr-protections-panel-link-text = Learn more
## What's New toolbar button and panel
cfr-whatsnew-button =

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

@ -11,7 +11,6 @@
## avoid breaking quoted text).
onboarding-button-label-learn-more = Learn More
onboarding-button-label-try-now = Try It Now
onboarding-button-label-get-started = Get Started
## Welcome modal dialog strings
@ -76,21 +75,6 @@ onboarding-benefit-privacy-text = Everything we do honors our Personal Data Prom
## Each message has a title and a description of what the browser feature is.
## Each message also has an associated button for the user to try the feature.
## The string for the button is found above, in the UI strings section
onboarding-private-browsing-title = Private Browsing
onboarding-private-browsing-text = Browse by yourself. Private Browsing with Content Blocking blocks online trackers that follow you around the web.
onboarding-screenshots-title = Screenshots
onboarding-screenshots-text = Take, save and share screenshots - without leaving { -brand-short-name }. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.
onboarding-addons-title = Add-ons
onboarding-addons-text = Add even more features that make { -brand-short-name } work harder for you. Compare prices, check the weather or express your personality with a custom theme.
onboarding-ghostery-title = Ghostery
onboarding-ghostery-text = Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.
# Note: "Sync" in this case is a generic verb, as in "to synchronize"
onboarding-fxa-title = Sync
onboarding-fxa-text = Sign up for a { -fxaccount-brand-name } and sync your bookmarks, passwords, and open tabs everywhere you use { -brand-short-name }.
onboarding-tracking-protection-title2 = Protection From Tracking
onboarding-tracking-protection-text2 = { -brand-short-name } helps stop websites from tracking you online, making it harder for ads to follow you around the web.