diff --git a/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss b/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss index 6100691aaaab..3a44c9f65ae9 100644 --- a/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss +++ b/browser/components/newtab/content-src/asrouter/components/Button/_Button.scss @@ -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; + } } } diff --git a/browser/components/newtab/content-src/asrouter/docs/user-actions.md b/browser/components/newtab/content-src/asrouter/docs/user-actions.md new file mode 100644 index 000000000000..8893c1c6b04d --- /dev/null +++ b/browser/components/newtab/content-src/asrouter/docs/user-actions.md @@ -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" + } + } +} +``` diff --git a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss index f542ab958b19..7add9706ec1c 100644 --- a/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss +++ b/browser/components/newtab/content-src/asrouter/templates/SimpleBelowSearchSnippet/_SimpleBelowSearchSnippet.scss @@ -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%; } 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 22652a5a4ec4..ee6f59139a9f 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/CardGrid/CardGrid.jsx @@ -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} /> diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx index cfb7d1272a79..20d49a936460 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/DSCard.jsx @@ -17,6 +17,7 @@ export const DefaultMeta = ({ context, context_type, cta, + engagement, }) => (
@@ -29,7 +30,11 @@ export const DefaultMeta = ({
)}
- + ); @@ -40,6 +45,7 @@ export const VariantMeta = ({ context, context_type, cta, + engagement, sponsor, }) => (
@@ -51,9 +57,13 @@ export const VariantMeta = ({
{title}
{excerpt &&

{excerpt}

}
- {cta && } + {context && cta && } {!context && ( - + )} ); @@ -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 ( +
+ ); + } return ( -
+
@@ -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} /> - {!this.props.placeholder && ( - - )} +
); } diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss index 2791edc9f07a..cfec408c8bd9 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSCard/_DSCard.scss @@ -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 { diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx index e9976bc5b881..a7bd0b9ba3fd 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/DSContextFooter.jsx @@ -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 (
{context &&

{context}

} - {!context && context_type && ( + {!context && (context_type || engagement) && ( - + {engagement && !context_type ? ( +
{engagement}
+ ) : ( + + )}
)}
diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss index d0bdb65c0664..4c4aa7b93e59 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/DSContextFooter/_DSContextFooter.scss @@ -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; diff --git a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx index f4c99fc025c6..79d15f3f9ec2 100644 --- a/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx +++ b/browser/components/newtab/content-src/components/DiscoveryStreamComponents/Hero/Hero.jsx @@ -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 {
) ); diff --git a/browser/components/newtab/css/activity-stream-linux.css b/browser/components/newtab/css/activity-stream-linux.css index da0305a10a78..0db2caf10538 100644 --- a/browser/components/newtab/css/activity-stream-linux.css +++ b/browser/components/newtab/css/activity-stream-linux.css @@ -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%; } } diff --git a/browser/components/newtab/css/activity-stream-mac.css b/browser/components/newtab/css/activity-stream-mac.css index 8a4e050fb280..4ca26d997107 100644 --- a/browser/components/newtab/css/activity-stream-mac.css +++ b/browser/components/newtab/css/activity-stream-mac.css @@ -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%; } } diff --git a/browser/components/newtab/css/activity-stream-windows.css b/browser/components/newtab/css/activity-stream-windows.css index 27687c3435a6..26a491a8e725 100644 --- a/browser/components/newtab/css/activity-stream-windows.css +++ b/browser/components/newtab/css/activity-stream-windows.css @@ -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%; } } diff --git a/browser/components/newtab/data/content/activity-stream.bundle.js b/browser/components/newtab/data/content/activity-stream.bundle.js index 4e48d2cebf8c..5d410ff0c9b3 100644 --- a/browser/components/newtab/data/content/activity-stream.bundle.js +++ b/browser/components/newtab/data/content/activity-stream.bundle.js @@ -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: [{ diff --git a/browser/components/newtab/lib/OnboardingMessageProvider.jsm b/browser/components/newtab/lib/OnboardingMessageProvider.jsm index 95e0acd180b9..caed83722d45 100644 --- a/browser/components/newtab/lib/OnboardingMessageProvider.jsm +++ b/browser/components/newtab/lib/OnboardingMessageProvider.jsm @@ -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`, diff --git a/browser/components/newtab/lib/ToolbarBadgeHub.jsm b/browser/components/newtab/lib/ToolbarBadgeHub.jsm index 9a3c91950be9..f2f2cc3efcfe 100644 --- a/browser/components/newtab/lib/ToolbarBadgeHub.jsm +++ b/browser/components/newtab/lib/ToolbarBadgeHub.jsm @@ -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); diff --git a/browser/components/newtab/lib/ToolbarPanelHub.jsm b/browser/components/newtab/lib/ToolbarPanelHub.jsm index 2d00a138e274..74a6a64f751c 100644 --- a/browser/components/newtab/lib/ToolbarPanelHub.jsm +++ b/browser/components/newtab/lib/ToolbarPanelHub.jsm @@ -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"); diff --git a/browser/components/newtab/locales-src/asrouter.ftl b/browser/components/newtab/locales-src/asrouter.ftl index 882f15e2e2c1..a2b1d4926fa4 100644 --- a/browser/components/newtab/locales-src/asrouter.ftl +++ b/browser/components/newtab/locales-src/asrouter.ftl @@ -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 = diff --git a/browser/components/newtab/locales-src/onboarding.ftl b/browser/components/newtab/locales-src/onboarding.ftl index 3651d6a0c991..d05bcccc2ce0 100644 --- a/browser/components/newtab/locales-src/onboarding.ftl +++ b/browser/components/newtab/locales-src/onboarding.ftl @@ -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. diff --git a/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx index a04508c541df..337e6409c8ba 100644 --- a/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx +++ b/browser/components/newtab/test/unit/asrouter/templates/FirstRun.test.jsx @@ -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/" }, diff --git a/browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx index 9eebf9af7102..928b6b981b7c 100644 --- a/browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx +++ b/browser/components/newtab/test/unit/asrouter/templates/OnboardingMessage.test.jsx @@ -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" }, }, }; diff --git a/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx index 3817b9005e53..73770d0d37ea 100644 --- a/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx +++ b/browser/components/newtab/test/unit/asrouter/templates/Trailhead.test.jsx @@ -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/" }, diff --git a/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx b/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx index 7698112e6c23..43fc27f40ba2 100644 --- a/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx +++ b/browser/components/newtab/test/unit/asrouter/templates/Triplets.test.jsx @@ -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/" }, diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx index fda636bd2f46..23821f83c0eb 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSCard.test.jsx @@ -20,6 +20,7 @@ describe("", () => { beforeEach(() => { wrapper = shallow(); + wrapper.setState({ isSeen: true }); sandbox = sinon.createSandbox(); }); @@ -79,6 +80,7 @@ describe("", () => { it("should render badges for pocket, bookmark when not a spoc element ", () => { wrapper = mount(); + wrapper.setState({ isSeen: true }); const contextFooter = wrapper.find(DSContextFooter); assert.lengthOf(contextFooter.find(StatusMessage), 1); @@ -87,6 +89,7 @@ describe("", () => { it("should render Sponsored Context for a spoc element", () => { const context = "Sponsored by Foo"; wrapper = mount(); + wrapper.setState({ isSeen: true }); const contextFooter = wrapper.find(DSContextFooter); assert.lengthOf(contextFooter.find(StatusMessage), 0); @@ -99,6 +102,7 @@ describe("", () => { beforeEach(() => { dispatch = sandbox.stub(); wrapper = shallow(); + wrapper.setState({ isSeen: true }); }); it("should call dispatch with the correct events", () => { @@ -160,6 +164,7 @@ describe("", () => { describe("DSCard with CTA", () => { beforeEach(() => { wrapper = mount(); + wrapper.setState({ isSeen: true }); }); it("should render Default Meta", () => { @@ -178,9 +183,19 @@ describe("", () => { 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("", () => { assert.equal(meta.find(".source").text(), "Test ยท Sponsored"); }); }); + describe("DSCard with Intersection Observer", () => { + beforeEach(() => { + wrapper = shallow(); + }); + + 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(); + assert.isTrue(!!wrapper.instance().observer); + }); + }); }); describe(" component", () => { @@ -221,21 +274,21 @@ describe(" component", () => { it("should contain placeholder div", () => { const wrapper = shallow(); + wrapper.setState({ isSeen: true }); const card = wrapper.find("div.ds-card.placeholder"); assert.lengthOf(card, 1); }); it("should not be clickable", () => { const wrapper = shallow(); + 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(); + wrapper.setState({ isSeen: true }); const linkMenu = wrapper.find(DSLinkMenu); assert.lengthOf(linkMenu, 0); }); diff --git a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx index 232134e4dcf3..a36a0fcac14e 100644 --- a/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx +++ b/browser/components/newtab/test/unit/content-src/components/DiscoveryStreamComponents/DSContextFooter.test.jsx @@ -12,6 +12,7 @@ describe("", () => { const bookmarkBadge = "bookmark"; const removeBookmarkBadge = "removedBookmark"; const context = "Sponsored by Babel"; + const engagement = "Popular"; beforeEach(() => { wrapper = mount(); @@ -26,19 +27,32 @@ describe("", () => { assert.isTrue(wrapper.exists()); assert.isOk(wrapper.find(".story-footer")); }); + it("should render an engagement status if no badge and spoc passed", () => { + wrapper = mount(); + + 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(); + wrapper = mount( + + ); 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( - + ); + assert.lengthOf(wrapper.find(".story-view-count"), 0); assert.lengthOf(wrapper.find(StatusMessage), 0); assert.equal(wrapper.find(".story-sponsored-label").text(), context); }); diff --git a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js index 838995cf68eb..4761fa3a8e8b 100644 --- a/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js +++ b/browser/components/newtab/test/unit/lib/ToolbarBadgeHub.test.js @@ -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", () => { diff --git a/browser/locales/en-US/browser/newtab/asrouter.ftl b/browser/locales/en-US/browser/newtab/asrouter.ftl index 882f15e2e2c1..a2b1d4926fa4 100644 --- a/browser/locales/en-US/browser/newtab/asrouter.ftl +++ b/browser/locales/en-US/browser/newtab/asrouter.ftl @@ -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 = diff --git a/browser/locales/en-US/browser/newtab/onboarding.ftl b/browser/locales/en-US/browser/newtab/onboarding.ftl index 3651d6a0c991..d05bcccc2ce0 100644 --- a/browser/locales/en-US/browser/newtab/onboarding.ftl +++ b/browser/locales/en-US/browser/newtab/onboarding.ftl @@ -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.