Bug 1549863 - Add 3-card layout, card adjustments and bug fixes to Activity Stream r=r1cky

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ed Lee 2019-05-08 00:38:11 +00:00
Родитель 5730f5fc1c
Коммит 5fda4b07d2
422 изменённых файлов: 1775 добавлений и 1230 удалений

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

@ -100,9 +100,9 @@ function templateHTML(options, html) {
<link rel="stylesheet" href="${options.baseUrl}css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root">${isPrerendered ? html : "<!-- Regular React Rendering -->"}</div>
<div id="footer-asrouter-container"></div>${options.noscripts ? "" : scriptRender}
<div id="footer-asrouter-container" role="presentation"></div>${options.noscripts ? "" : scriptRender}
</body>
</html>
`;

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

@ -43,6 +43,7 @@ for (const type of [
"DISCOVERY_STREAM_CONFIG_SETUP",
"DISCOVERY_STREAM_CONFIG_SET_VALUE",
"DISCOVERY_STREAM_FEEDS_UPDATE",
"DISCOVERY_STREAM_FEED_UPDATE",
"DISCOVERY_STREAM_IMPRESSION_STATS",
"DISCOVERY_STREAM_LAYOUT_RESET",
"DISCOVERY_STREAM_LAYOUT_UPDATE",

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

@ -495,10 +495,22 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
...prevState,
feeds: {
...prevState.feeds,
data: action.data || prevState.feeds.data,
loaded: true,
},
};
case at.DISCOVERY_STREAM_FEED_UPDATE:
const newData = {};
newData[action.data.url] = action.data.feed;
return {
...prevState,
feeds: {
...prevState.feeds,
data: {
...prevState.feeds.data,
...newData,
},
},
};
case at.DISCOVERY_STREAM_SPOCS_CAPS:
return {
...prevState,

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

@ -296,6 +296,7 @@ export class ASRouterUISurface extends React.PureComponent {
const {message} = this.state;
if (message.template === "trailhead") {
return (<Trailhead
document={this.props.document}
message={message}
onAction={ASRouterUtils.executeAction}
onDoneButton={this.dismissBundle(this.state.bundle.bundle)}

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

@ -26,7 +26,10 @@ export class ModalOverlayWrapper extends React.PureComponent {
const {props} = this;
return (<React.Fragment>
<div className="modalOverlayOuter active" onClick={props.onClose} role="presentation" />
<div className={`modalOverlayInner active ${props.innerClassName || ""}`}>
<div className={`modalOverlayInner active ${props.innerClassName || ""}`}
aria-labelledby={props.headerId}
id={props.id}
role="dialog">
{props.children}
</div>
</React.Fragment>);

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

@ -13,6 +13,8 @@ export class _StartupOverlay extends React.PureComponent {
this.removeOverlay = this.removeOverlay.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this);
this.utmParams = "utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
this.state = {
emailInput: "",
overlayRemoved: false,
@ -26,8 +28,8 @@ export class _StartupOverlay extends React.PureComponent {
if (this.props.fxa_endpoint && !this.didFetch) {
try {
this.didFetch = true;
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}`, {credentials: "omit"});
const fxaParams = "entrypoint=activity-stream-firstrun&form_type=email";
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}&${this.utmParams}`, {credentials: "omit"});
if (response.status === 200) {
const {flowId, flowBeginTime} = await response.json();
this.setState({flowId, flowBeginTime});
@ -106,8 +108,8 @@ export class _StartupOverlay extends React.PureComponent {
return null;
}
let termsLink = (<a href={`${this.props.fxa_endpoint}/legal/terms`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_terms_of_service" /></a>);
let privacyLink = (<a href={`${this.props.fxa_endpoint}/legal/privacy`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_privacy_notice" /></a>);
let termsLink = (<a href={`${this.props.fxa_endpoint}/legal/terms?${this.utmParams}`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_terms_of_service" /></a>);
let privacyLink = (<a href={`${this.props.fxa_endpoint}/legal/privacy?${this.utmParams}`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_privacy_notice" /></a>);
return (
<div className={`overlay-wrapper ${this.state.show ? "show" : ""}`}>
@ -117,7 +119,7 @@ export class _StartupOverlay extends React.PureComponent {
<div className="firstrun-left-divider">
<h1 className="firstrun-title"><FormattedMessage id="firstrun_title" /></h1>
<p className="firstrun-content"><FormattedMessage id="firstrun_content" /></p>
<a className="firstrun-link" href="https://www.mozilla.org/firefox/features/sync/" target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_learn_more_link" /></a>
<a className="firstrun-link" href={`https://www.mozilla.org/firefox/features/sync/?${this.utmParams}`} target="_blank" rel="noopener noreferrer"><FormattedMessage id="firstrun_learn_more_link" /></a>
</div>
<div className="firstrun-sign-in">
<p className="form-header"><FormattedMessage id="firstrun_form_header" /><span className="sub-header"><FormattedMessage id="firstrun_form_sub_header" /></span></p>
@ -128,6 +130,8 @@ export class _StartupOverlay extends React.PureComponent {
<input name="entrypoint" type="hidden" value="activity-stream-firstrun" />
<input name="utm_source" type="hidden" value="activity-stream" />
<input name="utm_campaign" type="hidden" value="firstrun" />
<input name="utm_medium" type="hidden" value="referral" />
<input name="utm_term" type="hidden" value="trailhead-control" />
<input name="flow_id" type="hidden" value={this.state.flowId} />
<input name="flow_begin_time" type="hidden" value={this.state.flowBeginTime} />
<span className="error">{this.props.intl.formatMessage({id: "firstrun_invalid_input"})}</span>

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

@ -6,22 +6,32 @@ import React from "react";
const FLUENT_FILES = [
"branding/brand.ftl",
"browser/branding/brandings.ftl",
"browser/branding/sync-brand.ftl",
// These are finalized strings exposed to localizers
"browser/newtab/onboarding.ftl",
// These are WIP/in-development strings that only get used if the string
// doesn't already exist in onboarding.ftl above
"trailhead.ftl",
];
// From resource://devtools/client/shared/focus.js
const FOCUSABLE_SELECTOR = [
"a[href]:not([tabindex='-1'])",
"button:not([disabled]):not([tabindex='-1'])",
"iframe:not([tabindex='-1'])",
"input:not([disabled]):not([tabindex='-1'])",
"select:not([disabled]):not([tabindex='-1'])",
"textarea:not([disabled]):not([tabindex='-1'])",
"[tabindex]:not([tabindex='-1'])",
].join(", ");
export class _Trailhead extends React.PureComponent {
constructor(props) {
super(props);
this.closeModal = this.closeModal.bind(this);
this.hideCardPanel = this.hideCardPanel.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.onStartBlur = this.onStartBlur.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this);
this.onCardAction = this.onCardAction.bind(this);
this.state = {
emailInput: "",
@ -34,6 +44,10 @@ export class _Trailhead extends React.PureComponent {
this.didFetch = false;
}
get dialog() {
return this.props.document.getElementById("trailheadDialog");
}
async componentWillMount() {
FLUENT_FILES.forEach(file => {
const link = document.head.appendChild(document.createElement("link"));
@ -44,8 +58,9 @@ export class _Trailhead extends React.PureComponent {
if (this.props.fxaEndpoint && !this.didFetch) {
try {
this.didFetch = true;
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&utm_term=trailhead&form_type=email";
const response = await fetch(`${this.props.fxaEndpoint}/metrics-flow?${fxaParams}`, {credentials: "omit"});
const url = new URL(`${this.props.fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`);
this.addUtmParams(url);
const response = await fetch(url, {credentials: "omit"});
if (response.status === 200) {
const {flowId, flowBeginTime} = await response.json();
this.setState({flowId, flowBeginTime});
@ -60,14 +75,20 @@ export class _Trailhead extends React.PureComponent {
componentDidMount() {
// We need to remove hide-main since we should show it underneath everything that has rendered
global.document.body.classList.remove("hide-main");
this.props.document.body.classList.remove("hide-main");
// Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
global.document.body.classList.add("inline-onboarding");
this.props.document.body.classList.add("inline-onboarding");
if (!this.props.message.content) {
// The rest of the page is "hidden" when the modal is open
if (this.props.message.content) {
this.props.document.getElementById("root").setAttribute("aria-hidden", "true");
// Start with focus in the email input box
this.dialog.querySelector("input[name=email]").focus();
} else {
// No modal overlay, let the user scroll and deal them some cards.
global.document.body.classList.remove("welcome");
this.props.document.body.classList.remove("welcome");
if (this.props.message.includeBundle || this.props.message.cards) {
this.revealCards();
@ -75,8 +96,8 @@ export class _Trailhead extends React.PureComponent {
}
}
componentDidUnmount() {
global.document.body.classList.remove("inline-onboarding");
componentWillUnmount() {
this.props.document.body.classList.remove("inline-onboarding");
}
onInputChange(e) {
@ -86,6 +107,16 @@ export class _Trailhead extends React.PureComponent {
e.target.classList.remove("invalid");
}
onStartBlur(event) {
// Make sure focus stays within the dialog when tabbing from the button
const {dialog} = this;
if (event.relatedTarget &&
!(dialog.compareDocumentPosition(event.relatedTarget) &
dialog.DOCUMENT_POSITION_CONTAINED_BY)) {
dialog.querySelector(FOCUSABLE_SELECTOR).focus();
}
}
onSubmit() {
this.props.dispatch(ac.UserEvent({event: "SUBMIT_EMAIL", ...this._getFormInfo()}));
@ -94,7 +125,8 @@ export class _Trailhead extends React.PureComponent {
closeModal() {
global.removeEventListener("visibilitychange", this.closeModal);
global.document.body.classList.remove("welcome");
this.props.document.body.classList.remove("welcome");
this.props.document.getElementById("root").removeAttribute("aria-hidden");
this.setState({isModalOpen: false});
this.revealCards();
this.props.dispatch(ac.UserEvent({event: "SKIPPED_SIGNIN", ...this._getFormInfo()}));
@ -131,18 +163,55 @@ export class _Trailhead extends React.PureComponent {
return str.value;
}
/**
* Takes in a url as a string or URL object and returns a URL object with the
* utm_* parameters added to it. If a URL object is passed in, the paraemeters
* are added to it (the return value can be ignored in that case as it's the
* same object).
*/
addUtmParams(url, isCard = false) {
let returnUrl = url;
if (typeof returnUrl === "string") {
returnUrl = new URL(url);
}
returnUrl.searchParams.append("utm_source", "activity-stream");
returnUrl.searchParams.append("utm_campaign", "firstrun");
returnUrl.searchParams.append("utm_medium", "referral");
returnUrl.searchParams.append("utm_term", `${this.props.message.utm_term}${isCard ? "-card" : ""}`);
return returnUrl;
}
onCardAction(action) {
let actionUpdates = {};
if (action.type === "OPEN_URL") {
let url = new URL(action.data.args);
this.addUtmParams(url, true);
if (action.addFlowParams) {
url.searchParams.append("flow_id", this.state.flowId);
url.searchParams.append("flow_begin_time", this.state.flowBeginTime);
}
actionUpdates = {data: {...action.data, args: url}};
}
this.props.onAction({...action, ...actionUpdates});
}
render() {
const {props} = this;
const {bundle: cards, content} = props.message;
const {bundle: cards, content, utm_term} = props.message;
const innerClassName = [
"trailhead",
content && content.className,
].filter(v => v).join(" ");
return (<>
{this.state.isModalOpen && content ? <ModalOverlayWrapper innerClassName={innerClassName} onClose={this.closeModal}>
{this.state.isModalOpen && content ? <ModalOverlayWrapper innerClassName={innerClassName} onClose={this.closeModal} id="trailheadDialog" headerId="trailheadHeader">
<div className="trailheadInner">
<div className="trailheadContent">
<h1 data-l10n-id={content.title.string_id}>{this.getStringValue(content.title)}</h1>
<h1 data-l10n-id={content.title.string_id}
id="trailheadHeader">{this.getStringValue(content.title)}</h1>
{content.subtitle &&
<p data-l10n-id={content.subtitle.string_id}>{this.getStringValue(content.subtitle)}</p>
}
@ -154,7 +223,7 @@ export class _Trailhead extends React.PureComponent {
</li>
))}
</ul>
<a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={content.learn.url}>
<a className="trailheadLearn" data-l10n-id={content.learn.text.string_id} href={this.addUtmParams(content.learn.url)}>
{this.getStringValue(content.learn.text)}
</a>
</div>
@ -168,9 +237,10 @@ export class _Trailhead extends React.PureComponent {
<input name="entrypoint" type="hidden" value="activity-stream-firstrun" />
<input name="utm_source" type="hidden" value="activity-stream" />
<input name="utm_campaign" type="hidden" value="firstrun" />
<input name="utm_term" type="hidden" value="trailhead" />
<input name="utm_term" type="hidden" value={utm_term} />
<input name="flow_id" type="hidden" value={this.state.flowId} />
<input name="flow_begin_time" type="hidden" value={this.state.flowBeginTime} />
<input name="style" type="hidden" value="trailhead" />
<p data-l10n-id="onboarding-join-form-email-error" className="error" />
<input
data-l10n-id={content.form.email.string_id}
@ -182,9 +252,9 @@ export class _Trailhead extends React.PureComponent {
onChange={this.onInputChange} />
<p className="trailheadTerms" data-l10n-id="onboarding-join-form-legal">
<a data-l10n-name="terms"
href="https://accounts.firefox.com/legal/terms" />
href={this.addUtmParams("https://accounts.firefox.com/legal/terms")} />
<a data-l10n-name="privacy"
href="https://accounts.firefox.com/legal/privacy" />
href={this.addUtmParams("https://accounts.firefox.com/legal/privacy")} />
</p>
<button data-l10n-id={content.form.button.string_id} type="submit">
{this.getStringValue(content.form.button)}
@ -195,17 +265,19 @@ export class _Trailhead extends React.PureComponent {
<button className="trailheadStart"
data-l10n-id={content.skipButton.string_id}
onBlur={this.onStartBlur}
onClick={this.closeModal}>{this.getStringValue(content.skipButton)}</button>
</ModalOverlayWrapper> : null}
{(cards && cards.length) ? <div className={`trailheadCards ${this.state.showCardPanel ? "expanded" : "collapsed"}`}>
<div className="trailheadCardsInner">
<div className="trailheadCardsInner"
aria-hidden={!this.state.showCards}>
<h1 data-l10n-id="onboarding-welcome-header" />
<div className={`trailheadCardGrid${this.state.showCards ? " show" : ""}`}>
{cards.map(card => (
<OnboardingCard key={card.id}
className="trailheadCard"
sendUserActionTelemetry={props.sendUserActionTelemetry}
onAction={props.onAction}
onAction={this.onCardAction}
UISurface="TRAILHEAD"
{...card} />
))}

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

@ -10,11 +10,6 @@
height: auto;
top: 100px;
@media (max-height: 700px) {
position: absolute;
top: 20px;
}
a {
color: $white;
text-decoration: underline;
@ -183,18 +178,38 @@
button,
input {
border: 0;
width: 100%;
}
input {
background-color: $white;
border: 1px solid $grey-50;
box-shadow: none;
color: $grey-70;
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms;
&:hover {
border-color: $grey-90;
}
&:focus {
border-color: $blue-50;
box-shadow: 0 0 0 3px $email-input-focus;
}
&.invalid {
border-color: $red-60;
}
&.invalid:focus {
box-shadow: 0 0 0 3px $email-input-invalid;
}
}
button {
background-color: $blue-60;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
@ -401,9 +416,14 @@
}
}
// If the window is too short, we need to allow scrolling so user can get to Start Browsing button.
@media (max-height: 700px) {
// If the window is too short or narrow, we need to allow scrolling so user can get to Start Browsing button.
@media (max-height: 760px), (max-width: 924px) {
.activity-stream.welcome.inline-onboarding {
overflow: auto;
}
.trailhead {
position: absolute;
top: 20px;
}
}

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

@ -170,14 +170,11 @@ export class _DiscoveryStreamBase extends React.PureComponent {
render() {
// Select layout render data by adding spocs and position to recommendations
const {layoutRender, spocsFill} = selectLayoutRender(this.props.DiscoveryStream, this.props.Prefs.values, rickRollCache);
const {config, feeds, spocs} = this.props.DiscoveryStream;
if (!spocs.loaded || !feeds.loaded) {
return null;
}
const {config, spocs, feeds} = this.props.DiscoveryStream;
// Send SPOCS Fill if any. Note that it should not send it again if the same
// page gets re-rendered by state changes.
if (spocsFill.length && !this._spocsFillSent) {
if (spocs.loaded && feeds.loaded && spocsFill.length && !this._spocsFillSent) {
this.props.dispatch(ac.DiscoveryStreamSpocsFill({spoc_fills: spocsFill}));
this._spocsFillSent = true;
}
@ -208,6 +205,10 @@ export class _DiscoveryStreamBase extends React.PureComponent {
// Get "topstories" Section state for default values
const topStories = this.props.Sections.find(s => s.id === "topstories");
if (!topStories) {
return null;
}
// Extract TopSites to render before the rest and Message to use for header
const topSites = extractComponent("TopSites");
const message = extractComponent("Message") || {
@ -255,6 +256,9 @@ export class _DiscoveryStreamBase extends React.PureComponent {
<div key={`row-${rowIndex}`} className={`ds-column ds-column-${row.width}`}>
<div className="ds-column-grid">
{row.components.map((component, componentIndex) => {
if (!component) {
return null;
}
styles[rowIndex] = [...styles[rowIndex] || [], component.styles];
return (<div key={`component-${componentIndex}`}>
{this.renderComponent(component, row.width)}

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

@ -49,7 +49,7 @@ $col4-header-font-size: 14;
background: none;
.meta {
padding: 16px 0;
padding: 12px 0;
}
}
}
@ -79,7 +79,7 @@ $col4-header-font-size: 14;
}
&.ds-card-grid-divisible-by-4 .title {
@include limit-visibile-lines(3, 20, 14);
@include limit-visibile-lines(3, 20, 15);
}
}

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

@ -72,10 +72,11 @@ $excerpt-line-height: 20;
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 16px;
padding: 12px;
.info-wrap {
flex-grow: 1;
margin: 0 0 12px;
}
.title {
@ -92,11 +93,11 @@ $excerpt-line-height: 20;
.context,
.source {
@include dark-theme-only {
color: $teal-10;
color: $grey-40;
}
font-size: 13px;
color: $teal-80;
color: $grey-50;
}
}
@ -112,12 +113,12 @@ $excerpt-line-height: 20;
p {
@include dark-theme-only {
color: $grey-30;
color: $grey-10;
}
font-size: $excerpt-font-size * 1px;
line-height: $excerpt-line-height * 1px;
color: $grey-50;
margin: 8px 0 0;
color: $grey-90;
margin: 0;
}
}

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

@ -15,7 +15,12 @@ $card-header-in-hero-line-height: 20;
.excerpt {
@include limit-visibile-lines(3, 20, 14);
margin: 4px 0 8px;
@include dark-theme-only {
color: $grey-10;
}
color: $grey-90;
margin: 0 0 10px;
}
.ds-card:not(.placeholder) {
@ -119,7 +124,7 @@ $card-header-in-hero-line-height: 20;
@include limit-visibile-lines(4, 28, 22);
color: $grey-90;
margin-bottom: 8px;
margin-bottom: 0;
}
.context {
@ -132,11 +137,11 @@ $card-header-in-hero-line-height: 20;
.source {
@include dark-theme-only {
color: $teal-10;
color: $grey-40;
}
font-size: 13px;
color: $teal-80;
color: $grey-50;
margin-bottom: 0;
overflow-x: hidden;
text-overflow: ellipsis;
@ -212,7 +217,7 @@ $card-header-in-hero-line-height: 20;
}
.img {
margin-bottom: 16px;
margin-bottom: 12px;
height: 0;
padding-top: 50%; // 2:1 aspect ratio
}

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

@ -220,10 +220,10 @@ $item-line-height: 20;
.ds-list-item-context {
@include limit-visibile-lines(1, $item-line-height, $item-font-size);
@include dark-theme-only {
color: $teal-10;
color: $grey-40;
}
color: $teal-80;
color: $grey-50;
font-size: 13px;
text-overflow: ellipsis;
}

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

@ -6,44 +6,49 @@ export const selectLayoutRender = (state, prefs, rickRollCache) => {
let chosenSpocs = new Set();
let unchosenSpocs = new Set();
// rickRollCache stores random probability values for each spoc position. This cache is empty
// on page refresh and gets filled with random values on first render inside maybeInjectSpocs.
const isFirstRun = !rickRollCache.length;
function maybeInjectSpocs(data, spocsConfig) {
if (data &&
spocsConfig && spocsConfig.positions && spocsConfig.positions.length &&
spocs.data.spocs && spocs.data.spocs.length) {
const recommendations = [...data.recommendations];
for (let position of spocsConfig.positions) {
const spoc = spocs.data.spocs[spocIndex];
if (!spoc) {
break;
}
// Cache random number for a position
let rickRoll;
if (isFirstRun) {
rickRoll = Math.random();
rickRollCache.push(rickRoll);
} else {
rickRoll = rickRollCache.shift();
bufferRollCache.push(rickRoll);
}
if (rickRoll <= spocsConfig.probability) {
spocIndex++;
recommendations.splice(position.index, 0, spoc);
chosenSpocs.add(spoc);
} else {
unchosenSpocs.add(spoc);
}
function rollForSpocs(data, spocsConfig) {
const recommendations = [...data.recommendations];
for (let position of spocsConfig.positions) {
const spoc = spocs.data.spocs[spocIndex];
if (!spoc) {
break;
}
return {
...data,
recommendations,
};
// Cache random number for a position
let rickRoll;
if (!rickRollCache.length) {
rickRoll = Math.random();
bufferRollCache.push(rickRoll);
} else {
rickRoll = rickRollCache.shift();
bufferRollCache.push(rickRoll);
}
if (rickRoll <= spocsConfig.probability) {
spocIndex++;
recommendations.splice(position.index, 0, spoc);
chosenSpocs.add(spoc);
} else {
unchosenSpocs.add(spoc);
}
}
return {
...data,
recommendations,
};
}
function maybeInjectSpocs(data, spocsConfig) {
// Do we ever expect to possibly have a spoc.
if (data && spocsConfig && spocsConfig.positions && spocsConfig.positions.length) {
// We expect a spoc, spocs are loaded, but the server returned no spocs.
if (!spocs.data.spocs || !spocs.data.spocs.length) {
return data;
}
// We expect a spoc, spocs are loaded, and we have spocs available.
return rollForSpocs(data, spocsConfig);
}
return data;
@ -63,56 +68,73 @@ export const selectLayoutRender = (state, prefs, rickRollCache) => {
filterArray.push(...DS_COMPONENTS);
}
const layoutRender = layout.map(row => ({
...row,
const handleComponent = component => {
positions[component.type] = positions[component.type] || 0;
// Loops through desired components and adds a .data property
// containing data from feeds
components: row.components.filter(c => !filterArray.includes(c.type)).map(component => {
if (!component.feed || !feeds.data[component.feed.url]) {
return component;
let {data} = feeds.data[component.feed.url];
if (component && component.properties && component.properties.offset) {
data = {
...data,
recommendations: data.recommendations.slice(component.properties.offset),
};
}
data = maybeInjectSpocs(data, component.spocs);
let items = 0;
if (component.properties && component.properties.items) {
items = Math.min(component.properties.items, data.recommendations.length);
}
// loop through a component items
// Store the items position sequentially for multiple components of the same type.
// Example: A second card grid starts pos offset from the last card grid.
for (let i = 0; i < items; i++) {
data.recommendations[i].pos = positions[component.type]++;
}
return {...component, data};
};
const renderLayout = () => {
const renderedLayoutArray = [];
for (const row of layout.filter(r => r.components.length)) {
let components = [];
renderedLayoutArray.push({
...row,
components,
});
for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
if (component.feed) {
const spocsConfig = component.spocs;
// Are we still waiting on a feed/spocs, render what we have, and bail out early.
if (!feeds.data[component.feed.url] ||
(spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded)) {
return renderedLayoutArray;
}
components.push(handleComponent(component));
} else {
components.push(component);
}
}
}
return renderedLayoutArray;
};
positions[component.type] = positions[component.type] || 0;
const layoutRender = renderLayout(layout);
let {data} = feeds.data[component.feed.url];
if (component && component.properties && component.properties.offset) {
data = {
...data,
recommendations: data.recommendations.slice(component.properties.offset),
};
}
data = maybeInjectSpocs(data, component.spocs);
// If empty, fill rickRollCache with random probability values from bufferRollCache
if (!rickRollCache.length) {
rickRollCache.push(...bufferRollCache);
}
let items = 0;
if (component.properties && component.properties.items) {
items = Math.min(component.properties.items, data.recommendations.length);
}
// loop through a component items
// Store the items position sequentially for multiple components of the same type.
// Example: A second card grid starts pos offset from the last card grid.
for (let i = 0; i < items; i++) {
data.recommendations[i].pos = positions[component.type]++;
}
return {...component, data};
}),
})).filter(row => row.components.length);
// If empty, fill rickRollCache with random probability values from bufferRollCache
if (!rickRollCache.length) {
rickRollCache.push(...bufferRollCache);
}
// Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected
// by the `probability_selection` first, then gets chosen for the next position. For
// all other SPOCS that never went through the probabilistic selection, its reason will
// be "out_of_position".
let spocsFill = [];
if (spocs.data.spocs) {
if (spocs.loaded && feeds.loaded && spocs.data.spocs) {
const chosenSpocsFill = [...chosenSpocs]
.map(spoc => ({id: spoc.id, reason: "n/a", displayed: 1, full_recalc: 0}));
const unchosenSpocsFill = [...unchosenSpocs]

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

@ -148,7 +148,7 @@ body {
--trailhead-header-text-color: #{$white-60};
--trailhead-cards-background-color: #{$grey-90-10};
--trailhead-card-button-background-color: #{$grey-90-30};
--trailhead-card-button-background-hover-color: #{$grey-90-40};
--trailhead-card-button-background-active-color: #{$grey-90-50};
--trailhead-card-button-background-hover-color: #{$grey-90-50};
--trailhead-card-button-background-active-color: #{$grey-90-70};
}
}

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

@ -120,8 +120,8 @@ body {
--trailhead-header-text-color: rgba(255, 255, 255, 0.6);
--trailhead-cards-background-color: rgba(12, 12, 13, 0.1);
--trailhead-card-button-background-color: rgba(12, 12, 13, 0.3);
--trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4);
--trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.5); }
--trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.5);
--trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.7); }
.icon {
background-position: center center;
@ -1920,7 +1920,7 @@ main {
.ds-card-grid.ds-card-grid-no-border .ds-card {
background: none; }
.ds-card-grid.ds-card-grid-no-border .ds-card .meta {
padding: 16px 0; }
padding: 12px 0; }
.ds-column-5 .ds-card-grid,
.ds-column-6 .ds-card-grid,
.ds-column-7 .ds-card-grid,
@ -1946,9 +1946,9 @@ main {
.ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
.ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
.ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-4 .title {
font-size: 14px;
font-size: 15px;
line-height: 20px;
max-height: 4.28571em;
max-height: 4em;
overflow: hidden; }
.ds-card-grid.empty {
grid-template-columns: auto; }
@ -1965,7 +1965,10 @@ main {
line-height: 20px;
max-height: 4.28571em;
overflow: hidden;
margin: 4px 0 8px; }
color: #0C0C0D;
margin: 0 0 10px; }
[lwt-newtab-brighttext] .ds-hero .excerpt {
color: #F9F9FA; }
.ds-hero .ds-card:not(.placeholder) {
border: 0;
padding-bottom: 20px; }
@ -2032,7 +2035,7 @@ main {
max-height: 5.09091em;
overflow: hidden;
color: #0C0C0D;
margin-bottom: 8px; }
margin-bottom: 0; }
[lwt-newtab-brighttext] .ds-hero .wrapper .meta header {
color: #FFF; }
.ds-hero .wrapper .meta .context {
@ -2041,12 +2044,12 @@ main {
color: #A7FFFE; }
.ds-hero .wrapper .meta .source {
font-size: 13px;
color: #005A71;
color: #737373;
margin-bottom: 0;
overflow-x: hidden;
text-overflow: ellipsis; }
[lwt-newtab-brighttext] .ds-hero .wrapper .meta .source {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-column-5 .ds-hero .wrapper,
.ds-column-6 .ds-hero .wrapper,
.ds-column-7 .ds-hero .wrapper,
@ -2130,7 +2133,7 @@ main {
.ds-column-10 .ds-hero .wrapper .img,
.ds-column-11 .ds-hero .wrapper .img,
.ds-column-12 .ds-hero .wrapper .img {
margin-bottom: 16px;
margin-bottom: 12px;
height: 0;
padding-top: 50%; }
.ds-column-9 .ds-hero .wrapper .meta,
@ -2366,12 +2369,12 @@ main {
line-height: 20px;
max-height: 1.42857em;
overflow: hidden;
color: #005A71;
color: #737373;
font-size: 13px;
text-overflow: ellipsis; }
[lwt-newtab-brighttext] .ds-list-item .ds-list-item-info, [lwt-newtab-brighttext]
.ds-list-item .ds-list-item-context {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-list-item .ds-list-item-title {
font-weight: 600;
margin-bottom: 4px; }
@ -2625,9 +2628,10 @@ main {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 16px; }
padding: 12px; }
.ds-card .meta .info-wrap {
flex-grow: 1; }
flex-grow: 1;
margin: 0 0 12px; }
.ds-card .meta .title {
font-size: 17px;
line-height: 24px;
@ -2642,10 +2646,10 @@ main {
.ds-card .meta .context,
.ds-card .meta .source {
font-size: 13px;
color: #005A71; }
color: #737373; }
[lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
.ds-card .meta .source {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-card header {
line-height: 24px;
font-size: 17px;
@ -2655,10 +2659,10 @@ main {
.ds-card p {
font-size: 14px;
line-height: 20px;
color: #737373;
margin: 8px 0 0; }
color: #0C0C0D;
margin: 0; }
[lwt-newtab-brighttext] .ds-card p {
color: #D7D7DB; }
color: #F9F9FA; }
.ds-image {
display: block;
@ -3709,10 +3713,6 @@ a.firstrun-link {
color: #FFF;
height: auto;
top: 100px; }
@media (max-height: 700px) {
.trailhead {
position: absolute;
top: 20px; } }
.trailhead a {
color: #FFF;
text-decoration: underline; }
@ -3820,14 +3820,26 @@ a.firstrun-link {
z-index: 0; }
.trailhead .trailheadForm button,
.trailhead .trailheadForm input {
border: 0;
width: 100%; }
.trailhead .trailheadForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px; }
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.trailhead .trailheadForm input:hover {
border-color: #0C0C0D; }
.trailhead .trailheadForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.trailhead .trailheadForm input.invalid {
border-color: #D70022; }
.trailhead .trailheadForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.trailhead .trailheadForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
@ -3966,6 +3978,9 @@ a.firstrun-link {
.inline-onboarding .asrouter-toggle {
position: absolute; }
@media (max-height: 700px) {
@media (max-height: 760px), (max-width: 924px) {
.activity-stream.welcome.inline-onboarding {
overflow: auto; } }
overflow: auto; }
.trailhead {
position: absolute;
top: 20px; } }

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

@ -123,8 +123,8 @@ body {
--trailhead-header-text-color: rgba(255, 255, 255, 0.6);
--trailhead-cards-background-color: rgba(12, 12, 13, 0.1);
--trailhead-card-button-background-color: rgba(12, 12, 13, 0.3);
--trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4);
--trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.5); }
--trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.5);
--trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.7); }
.icon {
background-position: center center;
@ -1923,7 +1923,7 @@ main {
.ds-card-grid.ds-card-grid-no-border .ds-card {
background: none; }
.ds-card-grid.ds-card-grid-no-border .ds-card .meta {
padding: 16px 0; }
padding: 12px 0; }
.ds-column-5 .ds-card-grid,
.ds-column-6 .ds-card-grid,
.ds-column-7 .ds-card-grid,
@ -1949,9 +1949,9 @@ main {
.ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
.ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
.ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-4 .title {
font-size: 14px;
font-size: 15px;
line-height: 20px;
max-height: 4.28571em;
max-height: 4em;
overflow: hidden; }
.ds-card-grid.empty {
grid-template-columns: auto; }
@ -1968,7 +1968,10 @@ main {
line-height: 20px;
max-height: 4.28571em;
overflow: hidden;
margin: 4px 0 8px; }
color: #0C0C0D;
margin: 0 0 10px; }
[lwt-newtab-brighttext] .ds-hero .excerpt {
color: #F9F9FA; }
.ds-hero .ds-card:not(.placeholder) {
border: 0;
padding-bottom: 20px; }
@ -2035,7 +2038,7 @@ main {
max-height: 5.09091em;
overflow: hidden;
color: #0C0C0D;
margin-bottom: 8px; }
margin-bottom: 0; }
[lwt-newtab-brighttext] .ds-hero .wrapper .meta header {
color: #FFF; }
.ds-hero .wrapper .meta .context {
@ -2044,12 +2047,12 @@ main {
color: #A7FFFE; }
.ds-hero .wrapper .meta .source {
font-size: 13px;
color: #005A71;
color: #737373;
margin-bottom: 0;
overflow-x: hidden;
text-overflow: ellipsis; }
[lwt-newtab-brighttext] .ds-hero .wrapper .meta .source {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-column-5 .ds-hero .wrapper,
.ds-column-6 .ds-hero .wrapper,
.ds-column-7 .ds-hero .wrapper,
@ -2133,7 +2136,7 @@ main {
.ds-column-10 .ds-hero .wrapper .img,
.ds-column-11 .ds-hero .wrapper .img,
.ds-column-12 .ds-hero .wrapper .img {
margin-bottom: 16px;
margin-bottom: 12px;
height: 0;
padding-top: 50%; }
.ds-column-9 .ds-hero .wrapper .meta,
@ -2369,12 +2372,12 @@ main {
line-height: 20px;
max-height: 1.42857em;
overflow: hidden;
color: #005A71;
color: #737373;
font-size: 13px;
text-overflow: ellipsis; }
[lwt-newtab-brighttext] .ds-list-item .ds-list-item-info, [lwt-newtab-brighttext]
.ds-list-item .ds-list-item-context {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-list-item .ds-list-item-title {
font-weight: 600;
margin-bottom: 4px; }
@ -2628,9 +2631,10 @@ main {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 16px; }
padding: 12px; }
.ds-card .meta .info-wrap {
flex-grow: 1; }
flex-grow: 1;
margin: 0 0 12px; }
.ds-card .meta .title {
font-size: 17px;
line-height: 24px;
@ -2645,10 +2649,10 @@ main {
.ds-card .meta .context,
.ds-card .meta .source {
font-size: 13px;
color: #005A71; }
color: #737373; }
[lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
.ds-card .meta .source {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-card header {
line-height: 24px;
font-size: 17px;
@ -2658,10 +2662,10 @@ main {
.ds-card p {
font-size: 14px;
line-height: 20px;
color: #737373;
margin: 8px 0 0; }
color: #0C0C0D;
margin: 0; }
[lwt-newtab-brighttext] .ds-card p {
color: #D7D7DB; }
color: #F9F9FA; }
.ds-image {
display: block;
@ -3712,10 +3716,6 @@ a.firstrun-link {
color: #FFF;
height: auto;
top: 100px; }
@media (max-height: 700px) {
.trailhead {
position: absolute;
top: 20px; } }
.trailhead a {
color: #FFF;
text-decoration: underline; }
@ -3823,14 +3823,26 @@ a.firstrun-link {
z-index: 0; }
.trailhead .trailheadForm button,
.trailhead .trailheadForm input {
border: 0;
width: 100%; }
.trailhead .trailheadForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px; }
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.trailhead .trailheadForm input:hover {
border-color: #0C0C0D; }
.trailhead .trailheadForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.trailhead .trailheadForm input.invalid {
border-color: #D70022; }
.trailhead .trailheadForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.trailhead .trailheadForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
@ -3969,6 +3981,9 @@ a.firstrun-link {
.inline-onboarding .asrouter-toggle {
position: absolute; }
@media (max-height: 700px) {
@media (max-height: 760px), (max-width: 924px) {
.activity-stream.welcome.inline-onboarding {
overflow: auto; } }
overflow: auto; }
.trailhead {
position: absolute;
top: 20px; } }

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

@ -120,8 +120,8 @@ body {
--trailhead-header-text-color: rgba(255, 255, 255, 0.6);
--trailhead-cards-background-color: rgba(12, 12, 13, 0.1);
--trailhead-card-button-background-color: rgba(12, 12, 13, 0.3);
--trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.4);
--trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.5); }
--trailhead-card-button-background-hover-color: rgba(12, 12, 13, 0.5);
--trailhead-card-button-background-active-color: rgba(12, 12, 13, 0.7); }
.icon {
background-position: center center;
@ -1920,7 +1920,7 @@ main {
.ds-card-grid.ds-card-grid-no-border .ds-card {
background: none; }
.ds-card-grid.ds-card-grid-no-border .ds-card .meta {
padding: 16px 0; }
padding: 12px 0; }
.ds-column-5 .ds-card-grid,
.ds-column-6 .ds-card-grid,
.ds-column-7 .ds-card-grid,
@ -1946,9 +1946,9 @@ main {
.ds-column-10 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
.ds-column-11 .ds-card-grid.ds-card-grid-divisible-by-4 .title,
.ds-column-12 .ds-card-grid.ds-card-grid-divisible-by-4 .title {
font-size: 14px;
font-size: 15px;
line-height: 20px;
max-height: 4.28571em;
max-height: 4em;
overflow: hidden; }
.ds-card-grid.empty {
grid-template-columns: auto; }
@ -1965,7 +1965,10 @@ main {
line-height: 20px;
max-height: 4.28571em;
overflow: hidden;
margin: 4px 0 8px; }
color: #0C0C0D;
margin: 0 0 10px; }
[lwt-newtab-brighttext] .ds-hero .excerpt {
color: #F9F9FA; }
.ds-hero .ds-card:not(.placeholder) {
border: 0;
padding-bottom: 20px; }
@ -2032,7 +2035,7 @@ main {
max-height: 5.09091em;
overflow: hidden;
color: #0C0C0D;
margin-bottom: 8px; }
margin-bottom: 0; }
[lwt-newtab-brighttext] .ds-hero .wrapper .meta header {
color: #FFF; }
.ds-hero .wrapper .meta .context {
@ -2041,12 +2044,12 @@ main {
color: #A7FFFE; }
.ds-hero .wrapper .meta .source {
font-size: 13px;
color: #005A71;
color: #737373;
margin-bottom: 0;
overflow-x: hidden;
text-overflow: ellipsis; }
[lwt-newtab-brighttext] .ds-hero .wrapper .meta .source {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-column-5 .ds-hero .wrapper,
.ds-column-6 .ds-hero .wrapper,
.ds-column-7 .ds-hero .wrapper,
@ -2130,7 +2133,7 @@ main {
.ds-column-10 .ds-hero .wrapper .img,
.ds-column-11 .ds-hero .wrapper .img,
.ds-column-12 .ds-hero .wrapper .img {
margin-bottom: 16px;
margin-bottom: 12px;
height: 0;
padding-top: 50%; }
.ds-column-9 .ds-hero .wrapper .meta,
@ -2366,12 +2369,12 @@ main {
line-height: 20px;
max-height: 1.42857em;
overflow: hidden;
color: #005A71;
color: #737373;
font-size: 13px;
text-overflow: ellipsis; }
[lwt-newtab-brighttext] .ds-list-item .ds-list-item-info, [lwt-newtab-brighttext]
.ds-list-item .ds-list-item-context {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-list-item .ds-list-item-title {
font-weight: 600;
margin-bottom: 4px; }
@ -2625,9 +2628,10 @@ main {
display: flex;
flex-direction: column;
flex-grow: 1;
padding: 16px; }
padding: 12px; }
.ds-card .meta .info-wrap {
flex-grow: 1; }
flex-grow: 1;
margin: 0 0 12px; }
.ds-card .meta .title {
font-size: 17px;
line-height: 24px;
@ -2642,10 +2646,10 @@ main {
.ds-card .meta .context,
.ds-card .meta .source {
font-size: 13px;
color: #005A71; }
color: #737373; }
[lwt-newtab-brighttext] .ds-card .meta .context, [lwt-newtab-brighttext]
.ds-card .meta .source {
color: #A7FFFE; }
color: #B1B1B3; }
.ds-card header {
line-height: 24px;
font-size: 17px;
@ -2655,10 +2659,10 @@ main {
.ds-card p {
font-size: 14px;
line-height: 20px;
color: #737373;
margin: 8px 0 0; }
color: #0C0C0D;
margin: 0; }
[lwt-newtab-brighttext] .ds-card p {
color: #D7D7DB; }
color: #F9F9FA; }
.ds-image {
display: block;
@ -3709,10 +3713,6 @@ a.firstrun-link {
color: #FFF;
height: auto;
top: 100px; }
@media (max-height: 700px) {
.trailhead {
position: absolute;
top: 20px; } }
.trailhead a {
color: #FFF;
text-decoration: underline; }
@ -3820,14 +3820,26 @@ a.firstrun-link {
z-index: 0; }
.trailhead .trailheadForm button,
.trailhead .trailheadForm input {
border: 0;
width: 100%; }
.trailhead .trailheadForm input {
background-color: #FFF;
border: 1px solid #737373;
box-shadow: none;
color: #38383D;
font-size: 15px; }
font-size: 15px;
transition: border-color 150ms, box-shadow 150ms; }
.trailhead .trailheadForm input:hover {
border-color: #0C0C0D; }
.trailhead .trailheadForm input:focus {
border-color: #0A84FF;
box-shadow: 0 0 0 3px rgba(10, 132, 255, 0.3); }
.trailhead .trailheadForm input.invalid {
border-color: #D70022; }
.trailhead .trailheadForm input.invalid:focus {
box-shadow: 0 0 0 3px rgba(215, 0, 34, 0.3); }
.trailhead .trailheadForm button {
background-color: #0060DF;
border: 0;
cursor: pointer;
display: block;
font-size: 15px;
@ -3966,6 +3978,9 @@ a.firstrun-link {
.inline-onboarding .asrouter-toggle {
position: absolute; }
@media (max-height: 700px) {
@media (max-height: 760px), (max-width: 924px) {
.activity-stream.welcome.inline-onboarding {
overflow: auto; } }
overflow: auto; }
.trailhead {
position: absolute;
top: 20px; } }

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

@ -197,7 +197,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
// }
const actionTypes = {};
for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_OPT_OUT", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_INITIALIZED", "AS_ROUTER_PREF_CHANGED", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISCOVERY_STREAM_CONFIG_CHANGE", "DISCOVERY_STREAM_CONFIG_SETUP", "DISCOVERY_STREAM_CONFIG_SET_VALUE", "DISCOVERY_STREAM_FEEDS_UPDATE", "DISCOVERY_STREAM_FEED_UPDATE", "DISCOVERY_STREAM_IMPRESSION_STATS", "DISCOVERY_STREAM_LAYOUT_RESET", "DISCOVERY_STREAM_LAYOUT_UPDATE", "DISCOVERY_STREAM_LINK_BLOCKED", "DISCOVERY_STREAM_LOADED_CONTENT", "DISCOVERY_STREAM_OPT_OUT", "DISCOVERY_STREAM_SPOCS_CAPS", "DISCOVERY_STREAM_SPOCS_ENDPOINT", "DISCOVERY_STREAM_SPOCS_FILL", "DISCOVERY_STREAM_SPOCS_UPDATE", "DISCOVERY_STREAM_SPOC_IMPRESSION", "DOWNLOAD_CHANGED", "FAKE_FOCUS_SEARCH", "FILL_SEARCH_TERM", "HANDOFF_SEARCH_TO_AWESOMEBAR", "HIDE_SEARCH", "INIT", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_CTA", "POCKET_LINK_DELETED_OR_ARCHIVED", "POCKET_LOGGED_IN", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SHOW_SEARCH", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_PREVIEW_MODE", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
actionTypes[type] = type;
} // These are acceptable actions for AS Router messages to have. They can show up
// as call-to-action buttons in snippets, onboarding tour, etc.
@ -2201,6 +2201,7 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_8___default.a.Pur
if (message.template === "trailhead") {
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(_templates_Trailhead_Trailhead__WEBPACK_IMPORTED_MODULE_13__["Trailhead"], {
document: this.props.document,
message: message,
onAction: ASRouterUtils.executeAction,
onDoneButton: this.dismissBundle(this.state.bundle.bundle),
@ -2729,7 +2730,10 @@ class ModalOverlayWrapper extends react__WEBPACK_IMPORTED_MODULE_0___default.a.P
onClick: props.onClose,
role: "presentation"
}), react__WEBPACK_IMPORTED_MODULE_0___default.a.createElement("div", {
className: `modalOverlayInner active ${props.innerClassName || ""}`
className: `modalOverlayInner active ${props.innerClassName || ""}`,
"aria-labelledby": props.headerId,
id: props.id,
role: "dialog"
}, props.children));
}
@ -2988,6 +2992,7 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
this.initScene = this.initScene.bind(this);
this.removeOverlay = this.removeOverlay.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this);
this.utmParams = "utm_source=activity-stream&utm_campaign=firstrun&utm_medium=referral&utm_term=trailhead-control";
this.state = {
emailInput: "",
overlayRemoved: false,
@ -3001,8 +3006,8 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
if (this.props.fxa_endpoint && !this.didFetch) {
try {
this.didFetch = true;
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}`, {
const fxaParams = "entrypoint=activity-stream-firstrun&form_type=email";
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}&${this.utmParams}`, {
credentials: "omit"
});
@ -3121,14 +3126,14 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
}
let termsLink = react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
href: `${this.props.fxa_endpoint}/legal/terms`,
href: `${this.props.fxa_endpoint}/legal/terms?${this.utmParams}`,
target: "_blank",
rel: "noopener noreferrer"
}, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
id: "firstrun_terms_of_service"
}));
let privacyLink = react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
href: `${this.props.fxa_endpoint}/legal/privacy`,
href: `${this.props.fxa_endpoint}/legal/privacy?${this.utmParams}`,
target: "_blank",
rel: "noopener noreferrer"
}, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
@ -3154,7 +3159,7 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
id: "firstrun_content"
})), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("a", {
className: "firstrun-link",
href: "https://www.mozilla.org/firefox/features/sync/",
href: `https://www.mozilla.org/firefox/features/sync/?${this.utmParams}`,
target: "_blank",
rel: "noopener noreferrer"
}, react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement(react_intl__WEBPACK_IMPORTED_MODULE_1__["FormattedMessage"], {
@ -3199,6 +3204,14 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
name: "utm_campaign",
type: "hidden",
value: "firstrun"
}), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
name: "utm_medium",
type: "hidden",
value: "referral"
}), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
name: "utm_term",
type: "hidden",
value: "trailhead-control"
}), react__WEBPACK_IMPORTED_MODULE_3___default.a.createElement("input", {
name: "flow_id",
type: "hidden",
@ -3279,18 +3292,19 @@ function _extends() { _extends = Object.assign || function (target) { for (var i
const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/sync-brand.ftl", // These are finalized strings exposed to localizers
"browser/newtab/onboarding.ftl", // These are WIP/in-development strings that only get used if the string
// doesn't already exist in onboarding.ftl above
"trailhead.ftl"];
const FLUENT_FILES = ["branding/brand.ftl", "browser/branding/brandings.ftl", "browser/branding/sync-brand.ftl", "browser/newtab/onboarding.ftl"]; // From resource://devtools/client/shared/focus.js
const FOCUSABLE_SELECTOR = ["a[href]:not([tabindex='-1'])", "button:not([disabled]):not([tabindex='-1'])", "iframe:not([tabindex='-1'])", "input:not([disabled]):not([tabindex='-1'])", "select:not([disabled]):not([tabindex='-1'])", "textarea:not([disabled]):not([tabindex='-1'])", "[tabindex]:not([tabindex='-1'])"].join(", ");
class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureComponent {
constructor(props) {
super(props);
this.closeModal = this.closeModal.bind(this);
this.hideCardPanel = this.hideCardPanel.bind(this);
this.onInputChange = this.onInputChange.bind(this);
this.onStartBlur = this.onStartBlur.bind(this);
this.onSubmit = this.onSubmit.bind(this);
this.onInputInvalid = this.onInputInvalid.bind(this);
this.onCardAction = this.onCardAction.bind(this);
this.state = {
emailInput: "",
isModalOpen: true,
@ -3302,6 +3316,10 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
this.didFetch = false;
}
get dialog() {
return this.props.document.getElementById("trailheadDialog");
}
async componentWillMount() {
FLUENT_FILES.forEach(file => {
const link = document.head.appendChild(document.createElement("link"));
@ -3312,8 +3330,9 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
if (this.props.fxaEndpoint && !this.didFetch) {
try {
this.didFetch = true;
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&utm_term=trailhead&form_type=email";
const response = await fetch(`${this.props.fxaEndpoint}/metrics-flow?${fxaParams}`, {
const url = new URL(`${this.props.fxaEndpoint}/metrics-flow?entrypoint=activity-stream-firstrun&form_type=email`);
this.addUtmParams(url);
const response = await fetch(url, {
credentials: "omit"
});
@ -3348,13 +3367,17 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
componentDidMount() {
// We need to remove hide-main since we should show it underneath everything that has rendered
global.document.body.classList.remove("hide-main"); // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
this.props.document.body.classList.remove("hide-main"); // Add inline-onboarding class to disable fixed search header and fixed positioned settings icon
global.document.body.classList.add("inline-onboarding");
this.props.document.body.classList.add("inline-onboarding"); // The rest of the page is "hidden" when the modal is open
if (!this.props.message.content) {
if (this.props.message.content) {
this.props.document.getElementById("root").setAttribute("aria-hidden", "true"); // Start with focus in the email input box
this.dialog.querySelector("input[name=email]").focus();
} else {
// No modal overlay, let the user scroll and deal them some cards.
global.document.body.classList.remove("welcome");
this.props.document.body.classList.remove("welcome");
if (this.props.message.includeBundle || this.props.message.cards) {
this.revealCards();
@ -3362,8 +3385,8 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
}
}
componentDidUnmount() {
global.document.body.classList.remove("inline-onboarding");
componentWillUnmount() {
this.props.document.body.classList.remove("inline-onboarding");
}
onInputChange(e) {
@ -3375,6 +3398,17 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
e.target.classList.remove("invalid");
}
onStartBlur(event) {
// Make sure focus stays within the dialog when tabbing from the button
const {
dialog
} = this;
if (event.relatedTarget && !(dialog.compareDocumentPosition(event.relatedTarget) & dialog.DOCUMENT_POSITION_CONTAINED_BY)) {
dialog.querySelector(FOCUSABLE_SELECTOR).focus();
}
}
onSubmit() {
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({
event: "SUBMIT_EMAIL",
@ -3385,7 +3419,8 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
closeModal() {
global.removeEventListener("visibilitychange", this.closeModal);
global.document.body.classList.remove("welcome");
this.props.document.body.classList.remove("welcome");
this.props.document.getElementById("root").removeAttribute("aria-hidden");
this.setState({
isModalOpen: false
});
@ -3439,6 +3474,51 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
return str.value;
}
/**
* Takes in a url as a string or URL object and returns a URL object with the
* utm_* parameters added to it. If a URL object is passed in, the paraemeters
* are added to it (the return value can be ignored in that case as it's the
* same object).
*/
addUtmParams(url, isCard = false) {
let returnUrl = url;
if (typeof returnUrl === "string") {
returnUrl = new URL(url);
}
returnUrl.searchParams.append("utm_source", "activity-stream");
returnUrl.searchParams.append("utm_campaign", "firstrun");
returnUrl.searchParams.append("utm_medium", "referral");
returnUrl.searchParams.append("utm_term", `${this.props.message.utm_term}${isCard ? "-card" : ""}`);
return returnUrl;
}
onCardAction(action) {
let actionUpdates = {};
if (action.type === "OPEN_URL") {
let url = new URL(action.data.args);
this.addUtmParams(url, true);
if (action.addFlowParams) {
url.searchParams.append("flow_id", this.state.flowId);
url.searchParams.append("flow_begin_time", this.state.flowBeginTime);
}
actionUpdates = {
data: { ...action.data,
args: url
}
};
}
this.props.onAction({ ...action,
...actionUpdates
});
}
render() {
const {
@ -3446,18 +3526,22 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
} = this;
const {
bundle: cards,
content
content,
utm_term
} = props.message;
const innerClassName = ["trailhead", content && content.className].filter(v => v).join(" ");
return react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(react__WEBPACK_IMPORTED_MODULE_4___default.a.Fragment, null, this.state.isModalOpen && content ? react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement(_components_ModalOverlay_ModalOverlay__WEBPACK_IMPORTED_MODULE_2__["ModalOverlayWrapper"], {
innerClassName: innerClassName,
onClose: this.closeModal
onClose: this.closeModal,
id: "trailheadDialog",
headerId: "trailheadHeader"
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
className: "trailheadInner"
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
className: "trailheadContent"
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h1", {
"data-l10n-id": content.title.string_id
"data-l10n-id": content.title.string_id,
id: "trailheadHeader"
}, this.getStringValue(content.title)), content.subtitle && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
"data-l10n-id": content.subtitle.string_id
}, this.getStringValue(content.subtitle)), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("ul", {
@ -3472,7 +3556,7 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
}, this.getStringValue(item.text))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
className: "trailheadLearn",
"data-l10n-id": content.learn.text.string_id,
href: content.learn.url
href: this.addUtmParams(content.learn.url)
}, this.getStringValue(content.learn.text))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
className: "trailheadForm"
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h3", {
@ -3512,7 +3596,7 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
}), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
name: "utm_term",
type: "hidden",
value: "trailhead"
value: utm_term
}), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
name: "flow_id",
type: "hidden",
@ -3521,6 +3605,10 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
name: "flow_begin_time",
type: "hidden",
value: this.state.flowBeginTime
}), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("input", {
name: "style",
type: "hidden",
value: "trailhead"
}), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("p", {
"data-l10n-id": "onboarding-join-form-email-error",
className: "error"
@ -3537,21 +3625,23 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
"data-l10n-id": "onboarding-join-form-legal"
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
"data-l10n-name": "terms",
href: "https://accounts.firefox.com/legal/terms"
href: this.addUtmParams("https://accounts.firefox.com/legal/terms")
}), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("a", {
"data-l10n-name": "privacy",
href: "https://accounts.firefox.com/legal/privacy"
href: this.addUtmParams("https://accounts.firefox.com/legal/privacy")
})), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
"data-l10n-id": content.form.button.string_id,
type: "submit"
}, this.getStringValue(content.form.button))))), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
className: "trailheadStart",
"data-l10n-id": content.skipButton.string_id,
onBlur: this.onStartBlur,
onClick: this.closeModal
}, this.getStringValue(content.skipButton))) : null, cards && cards.length ? react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
className: `trailheadCards ${this.state.showCardPanel ? "expanded" : "collapsed"}`
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
className: "trailheadCardsInner"
className: "trailheadCardsInner",
"aria-hidden": !this.state.showCards
}, react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("h1", {
"data-l10n-id": "onboarding-welcome-header"
}), react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("div", {
@ -3560,7 +3650,7 @@ class _Trailhead extends react__WEBPACK_IMPORTED_MODULE_4___default.a.PureCompon
key: card.id,
className: "trailheadCard",
sendUserActionTelemetry: props.sendUserActionTelemetry,
onAction: props.onAction,
onAction: this.onCardAction,
UISurface: "TRAILHEAD"
}, card)))), this.state.showCardPanel && react__WEBPACK_IMPORTED_MODULE_4___default.a.createElement("button", {
className: "icon icon-dismiss",
@ -8338,45 +8428,53 @@ const selectLayoutRender = (state, prefs, rickRollCache) => {
let bufferRollCache = []; // Records the chosen and unchosen spocs by the probability selection.
let chosenSpocs = new Set();
let unchosenSpocs = new Set(); // rickRollCache stores random probability values for each spoc position. This cache is empty
// on page refresh and gets filled with random values on first render inside maybeInjectSpocs.
let unchosenSpocs = new Set();
const isFirstRun = !rickRollCache.length;
function rollForSpocs(data, spocsConfig) {
const recommendations = [...data.recommendations];
function maybeInjectSpocs(data, spocsConfig) {
if (data && spocsConfig && spocsConfig.positions && spocsConfig.positions.length && spocs.data.spocs && spocs.data.spocs.length) {
const recommendations = [...data.recommendations];
for (let position of spocsConfig.positions) {
const spoc = spocs.data.spocs[spocIndex];
for (let position of spocsConfig.positions) {
const spoc = spocs.data.spocs[spocIndex];
if (!spoc) {
break;
} // Cache random number for a position
if (!spoc) {
break;
} // Cache random number for a position
let rickRoll;
let rickRoll;
if (isFirstRun) {
rickRoll = Math.random();
rickRollCache.push(rickRoll);
} else {
rickRoll = rickRollCache.shift();
bufferRollCache.push(rickRoll);
}
if (rickRoll <= spocsConfig.probability) {
spocIndex++;
recommendations.splice(position.index, 0, spoc);
chosenSpocs.add(spoc);
} else {
unchosenSpocs.add(spoc);
}
if (!rickRollCache.length) {
rickRoll = Math.random();
bufferRollCache.push(rickRoll);
} else {
rickRoll = rickRollCache.shift();
bufferRollCache.push(rickRoll);
}
return { ...data,
recommendations
};
if (rickRoll <= spocsConfig.probability) {
spocIndex++;
recommendations.splice(position.index, 0, spoc);
chosenSpocs.add(spoc);
} else {
unchosenSpocs.add(spoc);
}
}
return { ...data,
recommendations
};
}
function maybeInjectSpocs(data, spocsConfig) {
// Do we ever expect to possibly have a spoc.
if (data && spocsConfig && spocsConfig.positions && spocsConfig.positions.length) {
// We expect a spoc, spocs are loaded, but the server returned no spocs.
if (!spocs.data.spocs || !spocs.data.spocs.length) {
return data;
} // We expect a spoc, spocs are loaded, and we have spocs available.
return rollForSpocs(data, spocsConfig);
}
return data;
@ -8394,56 +8492,77 @@ const selectLayoutRender = (state, prefs, rickRollCache) => {
filterArray.push(...DS_COMPONENTS);
}
const layoutRender = layout.map(row => ({ ...row,
// Loops through desired components and adds a .data property
// containing data from feeds
components: row.components.filter(c => !filterArray.includes(c.type)).map(component => {
if (!component.feed || !feeds.data[component.feed.url]) {
return component;
}
const handleComponent = component => {
positions[component.type] = positions[component.type] || 0;
let {
data
} = feeds.data[component.feed.url];
positions[component.type] = positions[component.type] || 0;
let {
data
} = feeds.data[component.feed.url];
if (component && component.properties && component.properties.offset) {
data = { ...data,
recommendations: data.recommendations.slice(component.properties.offset)
};
}
data = maybeInjectSpocs(data, component.spocs); // If empty, fill rickRollCache with random probability values from bufferRollCache
if (!rickRollCache.length) {
rickRollCache.push(...bufferRollCache);
}
let items = 0;
if (component.properties && component.properties.items) {
items = Math.min(component.properties.items, data.recommendations.length);
} // loop through a component items
// Store the items position sequentially for multiple components of the same type.
// Example: A second card grid starts pos offset from the last card grid.
for (let i = 0; i < items; i++) {
data.recommendations[i].pos = positions[component.type]++;
}
return { ...component,
data
if (component && component.properties && component.properties.offset) {
data = { ...data,
recommendations: data.recommendations.slice(component.properties.offset)
};
})
})).filter(row => row.components.length); // Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected
}
data = maybeInjectSpocs(data, component.spocs);
let items = 0;
if (component.properties && component.properties.items) {
items = Math.min(component.properties.items, data.recommendations.length);
} // loop through a component items
// Store the items position sequentially for multiple components of the same type.
// Example: A second card grid starts pos offset from the last card grid.
for (let i = 0; i < items; i++) {
data.recommendations[i].pos = positions[component.type]++;
}
return { ...component,
data
};
};
const renderLayout = () => {
const renderedLayoutArray = [];
for (const row of layout.filter(r => r.components.length)) {
let components = [];
renderedLayoutArray.push({ ...row,
components
});
for (const component of row.components.filter(c => !filterArray.includes(c.type))) {
if (component.feed) {
const spocsConfig = component.spocs; // Are we still waiting on a feed/spocs, render what we have, and bail out early.
if (!feeds.data[component.feed.url] || spocsConfig && spocsConfig.positions && spocsConfig.positions.length && !spocs.loaded) {
return renderedLayoutArray;
}
components.push(handleComponent(component));
} else {
components.push(component);
}
}
}
return renderedLayoutArray;
};
const layoutRender = renderLayout(layout); // If empty, fill rickRollCache with random probability values from bufferRollCache
if (!rickRollCache.length) {
rickRollCache.push(...bufferRollCache);
} // Generate the payload for the SPOCS Fill ping. Note that a SPOC could be rejected
// by the `probability_selection` first, then gets chosen for the next position. For
// all other SPOCS that never went through the probabilistic selection, its reason will
// be "out_of_position".
let spocsFill = [];
if (spocs.data.spocs) {
if (spocs.loaded && feeds.loaded && spocs.data.spocs) {
const chosenSpocsFill = [...chosenSpocs].map(spoc => ({
id: spoc.id,
reason: "n/a",
@ -8679,17 +8798,12 @@ class DiscoveryStreamBase_DiscoveryStreamBase extends external_React_default.a.P
} = selectLayoutRender(this.props.DiscoveryStream, this.props.Prefs.values, rickRollCache);
const {
config,
feeds,
spocs
} = this.props.DiscoveryStream;
if (!spocs.loaded || !feeds.loaded) {
return null;
} // Send SPOCS Fill if any. Note that it should not send it again if the same
spocs,
feeds
} = this.props.DiscoveryStream; // Send SPOCS Fill if any. Note that it should not send it again if the same
// page gets re-rendered by state changes.
if (spocsFill.length && !this._spocsFillSent) {
if (spocs.loaded && feeds.loaded && spocsFill.length && !this._spocsFillSent) {
this.props.dispatch(Actions["actionCreators"].DiscoveryStreamSpocsFill({
spoc_fills: spocsFill
}));
@ -8722,7 +8836,12 @@ class DiscoveryStreamBase_DiscoveryStreamBase extends external_React_default.a.P
}; // Get "topstories" Section state for default values
const topStories = this.props.Sections.find(s => s.id === "topstories"); // Extract TopSites to render before the rest and Message to use for header
const topStories = this.props.Sections.find(s => s.id === "topstories");
if (!topStories) {
return null;
} // Extract TopSites to render before the rest and Message to use for header
const topSites = extractComponent("TopSites");
const message = extractComponent("Message") || {
@ -8765,6 +8884,10 @@ class DiscoveryStreamBase_DiscoveryStreamBase extends external_React_default.a.P
}, external_React_default.a.createElement("div", {
className: "ds-column-grid"
}, row.components.map((component, componentIndex) => {
if (!component) {
return null;
}
styles[rowIndex] = [...(styles[rowIndex] || []), component.styles];
return external_React_default.a.createElement("div", {
key: `component-${componentIndex}`
@ -11786,7 +11909,7 @@ class CachedIterable {
}
// CONCATENATED MODULE: ./node_modules/fluent/src/fallback.js
function _asyncIterator(iterable) { var method; if (typeof Symbol !== "undefined") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); }
function _asyncIterator(iterable) { var method; if (typeof Symbol === "function") { if (Symbol.asyncIterator) { method = iterable[Symbol.asyncIterator]; if (method != null) return method.call(iterable); } if (Symbol.iterator) { method = iterable[Symbol.iterator]; if (method != null) return method.call(iterable); } } throw new TypeError("Object is not async iterable"); }
/*
* @overview
@ -13231,11 +13354,21 @@ function DiscoveryStream(prevState = INITIAL_STATE.DiscoveryStream, action) {
case Actions["actionTypes"].DISCOVERY_STREAM_FEEDS_UPDATE:
return { ...prevState,
feeds: { ...prevState.feeds,
data: action.data || prevState.feeds.data,
loaded: true
}
};
case Actions["actionTypes"].DISCOVERY_STREAM_FEED_UPDATE:
const newData = {};
newData[action.data.url] = action.data.feed;
return { ...prevState,
feeds: { ...prevState.feeds,
data: { ...prevState.feeds.data,
...newData
}
}
};
case Actions["actionTypes"].DISCOVERY_STREAM_SPOCS_CAPS:
return { ...prevState,
spocs: { ...prevState.spocs,

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

@ -1000,7 +1000,7 @@ This reports the user's interaction with Activity Stream Router.
"source": "CFR",
// message_id could be the ID of the recommendation, such as "wikipedia_addon"
"message_id": "wikipedia_addon",
"event": "[INSTALL | PIN | BLOCK | DISMISS | RATIONALE | LEARN_MORE | CLICK_DOORHANGER | MANAGE]"
"event": "[IMPRESSION | INSTALL | PIN | BLOCK | DISMISS | RATIONALE | LEARN_MORE | CLICK | CLICK_DOORHANGER | MANAGE]"
}
```
@ -1016,7 +1016,7 @@ This reports the user's interaction with Activity Stream Router.
// message_id should be a bucket ID in the release channel, we may not use the
// individual ID, such as addon ID, per legal's request
"message_id": "bucket_id",
"event": "[INSTALL | PIN | BLOCK | DISMISS | RATIONALE | LEARN_MORE | CLICK_DOORHANGER | MANAGE]"
"event": "[IMPRESSION | INSTALL | PIN | BLOCK | DISMISS | RATIONALE | LEARN_MORE | CLICK | CLICK_DOORHANGER | MANAGE]"
}
```

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

@ -2,9 +2,6 @@
# 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/.
[localization] en-US.jar:
trailhead.ftl (./data/trailhead.wip)
browser.jar:
% resource activity-stream %res/activity-stream/ contentaccessible=yes
res/activity-stream/lib/ (./lib/*)

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

@ -41,6 +41,7 @@ ChromeUtils.defineModuleGetter(this, "Sampling",
const TRAILHEAD_CONFIG = {
OVERRIDE_PREF: "trailhead.firstrun.branches",
DID_SEE_ABOUT_WELCOME_PREF: "trailhead.firstrun.didSeeAboutWelcome",
INTERRUPTS_EXPERIMENT_PREF: "trailhead.firstrun.interruptsExperiment",
BRANCHES: {
interrupts: [
["control"],
@ -569,7 +570,7 @@ class _ASRouter {
ASRouterPreferences.init();
ASRouterPreferences.addListener(this.onPrefChange);
BookmarkPanelHub.init(this.handleMessageRequest, this.addImpression);
BookmarkPanelHub.init(this.handleMessageRequest, this.addImpression, this.dispatch);
this._loadLocalProviders();
@ -747,6 +748,13 @@ class _ASRouter {
experiment === "interrupts" ? interrupt : triplet,
{type: "as-firstrun"}
);
// On the first time setting the interrupts experiment, expose the branch
// for normandy to target for survey study.
if (experiment === "interrupts" &&
!Services.prefs.prefHasUserValue(TRAILHEAD_CONFIG.INTERRUPTS_EXPERIMENT_PREF)) {
Services.prefs.setStringPref(TRAILHEAD_CONFIG.INTERRUPTS_EXPERIMENT_PREF, interrupt);
}
}
}

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

@ -160,7 +160,7 @@ const PREFS_CONFIG = new Map([
value: "topsites,topstories,highlights",
}],
["improvesearch.noDefaultSearchTile", {
title: "Experiment to remove tiles that are the same as the default search",
title: "Remove tiles that are the same as the default search",
value: true,
}],
["improvesearch.topSiteSearchShortcuts.searchEngines", {
@ -215,7 +215,7 @@ const PREFS_CONFIG = new Map([
title: "Configuration for CFR FxA Messages provider",
value: JSON.stringify({
id: "cfr-fxa",
enabled: false,
enabled: true,
type: "remote-settings",
bucket: "cfr-fxa",
frequency: {custom: [{period: "daily", cap: 1}]},

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

@ -16,22 +16,26 @@ class _BookmarkPanelHub {
this._trigger = {id: "bookmark-panel"};
this._handleMessageRequest = null;
this._addImpression = null;
this._dispatch = null;
this._initalized = false;
this._response = null;
this._l10n = null;
this.messageRequest = this.messageRequest.bind(this);
this.toggleRecommendation = this.toggleRecommendation.bind(this);
this.sendUserEventTelemetry = this.sendUserEventTelemetry.bind(this);
this.collapseMessage = this.collapseMessage.bind(this);
}
/**
* @param {function} handleMessageRequest
* @param {function} addImpression
* @param {function} dispatch - Used for sending user telemetry information
*/
init(handleMessageRequest, addImpression) {
init(handleMessageRequest, addImpression, dispatch) {
this._handleMessageRequest = handleMessageRequest;
this._addImpression = addImpression;
this._dispatch = dispatch;
this._l10n = new DOMLocalization([
"browser/branding/sync-brand.ftl",
"browser/newtab/asrouter.ftl",
@ -44,6 +48,7 @@ class _BookmarkPanelHub {
this._initalized = false;
this._handleMessageRequest = null;
this._addImpression = null;
this._dispatch = null;
this._response = null;
}
@ -83,6 +88,7 @@ class _BookmarkPanelHub {
if (response && response.content) {
this.showMessage(response.content, target, win);
this.sendImpression();
this.sendUserEventTelemetry("IMPRESSION");
} else {
this.hideMessage(target);
}
@ -111,6 +117,7 @@ class _BookmarkPanelHub {
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({}),
csp: null,
});
this.sendUserEventTelemetry("CLICK");
});
recommendation.style.color = message.color;
recommendation.style.background = `-moz-linear-gradient(-45deg, ${message.background_color_1} 0%, ${message.background_color_2} 70%)`;
@ -119,6 +126,7 @@ class _BookmarkPanelHub {
close.setAttribute("aria-label", "close");
this._l10n.setAttributes(close, message.close_button.tooltiptext);
close.addEventListener("click", e => {
this.sendUserEventTelemetry("DISMISS");
this.collapseMessage();
target.close(e);
});
@ -181,6 +189,17 @@ class _BookmarkPanelHub {
sendImpression() {
this._addImpression(this._response);
}
sendUserEventTelemetry(event) {
this._sendTelemetry({message_id: this._response.id, bucket_id: this._response.id, event});
}
_sendTelemetry(ping) {
this._dispatch({
type: "DOORHANGER_TELEMETRY",
data: {action: "cfr_user_event", source: "CFR", ...ping},
});
}
}
this._BookmarkPanelHub = _BookmarkPanelHub;

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

@ -266,7 +266,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
* the scope for isStartup and the promises object.
* Combines feed results and promises for each component with a feed.
*/
buildFeedPromise({newFeedsPromises, newFeeds}, isStartup) {
buildFeedPromise({newFeedsPromises, newFeeds}, isStartup, sendUpdate) {
return component => {
const {url} = component.feed;
@ -275,8 +275,16 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
// we then fill in with the proper object inside the promise.
newFeeds[url] = {};
const feedPromise = this.getComponentFeed(url, isStartup);
feedPromise.then(feed => {
newFeeds[url] = this.filterRecommendations(feed);
sendUpdate({
type: at.DISCOVERY_STREAM_FEED_UPDATE,
data: {
feed: newFeeds[url],
url,
},
});
// We grab affinities off the first feed for the moment.
// Ideally this would be returned from the server on the layout,
@ -293,7 +301,6 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
}).catch(/* istanbul ignore next */ error => {
Cu.reportError(`Error trying to load component feed ${url}: ${error}`);
});
newFeedsPromises.push(feedPromise);
}
};
@ -317,11 +324,11 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
* @returns {Function} We return a function so we can contain the scope for isStartup.
* Reduces feeds into promises and feed data.
*/
reduceFeedComponents(isStartup) {
reduceFeedComponents(isStartup, sendUpdate) {
return (accumulator, row) => {
row.components
.filter(component => component && component.feed)
.forEach(this.buildFeedPromise(accumulator, isStartup));
.forEach(this.buildFeedPromise(accumulator, isStartup, sendUpdate));
return accumulator;
};
}
@ -334,14 +341,14 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
* @returns {Object} An object with newFeedsPromises (Array) and newFeeds (Object),
* we can Promise.all newFeedsPromises to get completed data in newFeeds.
*/
buildFeedPromises(layout, isStartup) {
buildFeedPromises(layout, isStartup, sendUpdate) {
const initialData = {
newFeedsPromises: [],
newFeeds: {},
};
return layout
.filter(row => row && row.components)
.reduce(this.reduceFeedComponents(isStartup), initialData);
.reduce(this.reduceFeedComponents(isStartup, sendUpdate), initialData);
}
async loadComponentFeeds(sendUpdate, isStartup) {
@ -355,7 +362,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
// was issued to fetch the component feed in `getComponentFeed()`.
this.componentFeedFetched = false;
const start = perfService.absNow();
const {newFeedsPromises, newFeeds} = this.buildFeedPromises(DiscoveryStream.layout, isStartup);
const {newFeedsPromises, newFeeds} = this.buildFeedPromises(DiscoveryStream.layout, isStartup, sendUpdate);
// Each promise has a catch already built in, so no need to catch here.
await Promise.all(newFeedsPromises);
@ -365,7 +372,9 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
this.componentFeedRequestTime = Math.round(perfService.absNow() - start);
}
await this.cache.set("feeds", newFeeds);
sendUpdate({type: at.DISCOVERY_STREAM_FEEDS_UPDATE, data: newFeeds});
sendUpdate({
type: at.DISCOVERY_STREAM_FEEDS_UPDATE,
});
}
async loadSpocs(sendUpdate, isStartup) {
@ -615,6 +624,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
async getComponentFeed(feedUrl, isStartup) {
const cachedData = await this.cache.get() || {};
const {feeds} = cachedData;
let feed = feeds ? feeds[feedUrl] : null;
if (this.isExpired({cachedData, key: "feed", url: feedUrl, isStartup})) {
const feedResponse = await this.fetchFromEndpoint(feedUrl);
@ -626,7 +636,7 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
feed = {
lastUpdated: Date.now(),
data: {
...feedResponse,
settings: feedResponse.settings,
recommendations,
},
};
@ -673,8 +683,8 @@ this.DiscoveryStreamFeed = class DiscoveryStreamFeed {
this.loadAffinityScoresCache();
await this.loadLayout(dispatch, isStartup);
await Promise.all([
this.loadComponentFeeds(dispatch, isStartup).catch(error => Cu.reportError(`Error trying to load component feeds: ${error}`)),
this.loadSpocs(dispatch, isStartup).catch(error => Cu.reportError(`Error trying to load spocs feed: ${error}`)),
this.loadComponentFeeds(dispatch, isStartup).catch(error => Cu.reportError(`Error trying to load component feeds: ${error}`)),
]);
if (isStartup) {
await this._maybeUpdateCachedData();
@ -1032,7 +1042,7 @@ defaultLayoutResp = {
{
"type": "CardGrid",
"properties": {
"items": 4,
"items": 3,
},
"header": {
"title": "",
@ -1045,7 +1055,7 @@ defaultLayoutResp = {
"probability": 1,
"positions": [
{
"index": 3,
"index": 2,
},
],
},
@ -1062,19 +1072,21 @@ defaultLayoutResp = {
},
"feed": {
"embed_reference": null,
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=4&duration=2592000&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=4&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
},
"properties": {
"items": 4,
"has_numbers": false,
"has_images": true,
"border": "no-border",
},
"styles": {
".ds-header": "margin-top: 4px;",
},
"spocs": {
"probability": 1,
"positions": [
{
"index": 3,
"index": 2,
},
],
},
@ -1091,13 +1103,15 @@ defaultLayoutResp = {
},
"feed": {
"embed_reference": null,
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=5&duration=2592000&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=5&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
},
"properties": {
"items": 4,
"has_numbers": false,
"has_images": true,
"border": "no-border",
},
"styles": {
".ds-header": "margin-top: 4px;",
},
},
],
@ -1112,13 +1126,15 @@ defaultLayoutResp = {
},
"feed": {
"embed_reference": null,
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=8&duration=2592000&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=8&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
},
"properties": {
"items": 4,
"has_numbers": false,
"has_images": true,
"border": "no-border",
},
"styles": {
".ds-header": "margin-top: 4px;",
},
"spocs": {
"probability": 1,
@ -1141,13 +1157,15 @@ defaultLayoutResp = {
},
"feed": {
"embed_reference": null,
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=2&duration=2592000&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=2&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
},
"styles": {
".ds-header": "margin-top: 4px;",
},
"properties": {
"items": 4,
"has_numbers": false,
"has_images": true,
"border": "no-border",
},
},
],
@ -1162,13 +1180,15 @@ defaultLayoutResp = {
},
"feed": {
"embed_reference": null,
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=1&duration=2592000&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=1&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
},
"properties": {
"items": 4,
"has_numbers": false,
"has_images": true,
"border": "no-border",
},
"styles": {
".ds-header": "margin-top: 4px;",
},
},
],
@ -1183,13 +1203,15 @@ defaultLayoutResp = {
},
"feed": {
"embed_reference": null,
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=7&duration=2592000&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
"url": "https://getpocket.cdn.mozilla.net/v3/firefox/global-recs?topic_id=7&end_time_offset=172800&version=3&consumer_key=$apiKey&locale_lang=en-US&feed_variant=OptimalCuratedLinksForLocaleFeed&model_id=external_time_live",
},
"properties": {
"items": 4,
"has_numbers": false,
"has_images": true,
"border": "no-border",
},
"styles": {
".ds-header": "margin-top: 4px;",
},
"spocs": {
"probability": 1,

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

@ -148,6 +148,7 @@ const ONBOARDING_MESSAGES = async () => ([
targeting: "trailheadInterrupt == 'join'",
trigger: {id: "firstRun"},
includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
utm_term: "trailhead-join",
content: {
className: "joinCohort",
title: {string_id: "onboarding-welcome-body"},
@ -177,6 +178,7 @@ const ONBOARDING_MESSAGES = async () => ([
targeting: "trailheadInterrupt == 'sync'",
trigger: {id: "firstRun"},
includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
utm_term: "trailhead-sync",
content: {
className: "syncCohort",
title: {property_id: "firstrun_title"},
@ -201,6 +203,7 @@ const ONBOARDING_MESSAGES = async () => ([
targeting: "trailheadInterrupt == 'cards'",
trigger: {id: "firstRun"},
includeBundle: {length: 3, template: "onboarding", trigger: {id: "showOnboarding"}},
utm_term: "trailhead-cards",
},
{
id: "TRAILHEAD_4",
@ -241,7 +244,8 @@ const ONBOARDING_MESSAGES = async () => ([
label: {string_id: "onboarding-data-sync-button"},
action: {
type: "OPEN_URL",
data: {args: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun", where: "tabshifted"},
addFlowParams: true,
data: {args: "https://accounts.firefox.com/?service=sync&action=email&context=fx_desktop_v3&entrypoint=activity-stream-firstrun&style=trailhead", where: "tabshifted"},
},
},
},
@ -274,11 +278,11 @@ const ONBOARDING_MESSAGES = async () => ([
bundled: 3,
order: 1,
content: {
title: {string_id: "onboarding-private-browsing-title"},
text: {string_id: "onboarding-private-browsing-text"},
title: {string_id: "onboarding-browse-privately-title"},
text: {string_id: "onboarding-browse-privately-text"},
icon: "private",
primary_button: {
label: {string_id: "onboarding-private-browsing-button"},
label: {string_id: "onboarding-browse-privately-button"},
action: {type: "OPEN_PRIVATE_BROWSER_WINDOW"},
},
},
@ -298,7 +302,7 @@ const ONBOARDING_MESSAGES = async () => ([
label: {string_id: "onboarding-firefox-send-button"},
action: {
type: "OPEN_URL",
data: {args: "https://send.firefox.com/?utm_source=activity-stream?utm_medium=referral?utm_campaign=firstrun", where: "tabshifted"},
data: {args: "https://send.firefox.com/", where: "tabshifted"},
},
},
},
@ -329,25 +333,6 @@ const ONBOARDING_MESSAGES = async () => ([
id: "TRAILHEAD_CARD_7",
template: "onboarding",
bundled: 3,
content: {
title: {string_id: "onboarding-privacy-right-title"},
text: {string_id: "onboarding-privacy-right-text"},
icon: "pledge",
primary_button: {
label: {string_id: "onboarding-privacy-right-button"},
action: {
type: "OPEN_URL",
data: {args: "https://www.mozilla.org/?privacy-right", where: "tabshifted"},
},
},
},
targeting: "trailheadTriplet == 'unused'",
trigger: {id: "showOnboarding"},
},
{
id: "TRAILHEAD_CARD_8",
template: "onboarding",
bundled: 3,
order: 3,
content: {
title: {string_id: "onboarding-send-tabs-title"},
@ -365,7 +350,7 @@ const ONBOARDING_MESSAGES = async () => ([
trigger: {id: "showOnboarding"},
},
{
id: "TRAILHEAD_CARD_9",
id: "TRAILHEAD_CARD_8",
template: "onboarding",
bundled: 3,
order: 2,
@ -385,7 +370,7 @@ const ONBOARDING_MESSAGES = async () => ([
trigger: {id: "showOnboarding"},
},
{
id: "TRAILHEAD_CARD_10",
id: "TRAILHEAD_CARD_9",
template: "onboarding",
bundled: 3,
order: 3,
@ -405,7 +390,7 @@ const ONBOARDING_MESSAGES = async () => ([
trigger: {id: "showOnboarding"},
},
{
id: "TRAILHEAD_CARD_11",
id: "TRAILHEAD_CARD_10",
template: "onboarding",
bundled: 3,
order: 4,

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

@ -42,7 +42,7 @@ const SECTION_ID = "topsites";
const ROWS_PREF = "topSitesRows";
// Search experiment stuff
const NO_DEFAULT_SEARCH_TILE_EXP_PREF = "improvesearch.noDefaultSearchTile";
const FILTER_DEFAULT_SEARCH_PREF = "improvesearch.noDefaultSearchTile";
const SEARCH_FILTERS = [
"google",
"search.yahoo",
@ -87,7 +87,7 @@ this.TopSitesFeed = class TopSitesFeed {
observe(subj, topic, data) {
// We should update the current top sites if the search engine has been changed since
// the search engine that gets filtered out of top sites has changed.
if (topic === "browser-search-engine-modified" && data === "engine-default" && this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF]) {
if (topic === "browser-search-engine-modified" && data === "engine-default" && this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF]) {
delete this._currentSearchHostname;
this._currentSearchHostname = getShortURLForCurrentSearch();
this.refresh({broadcast: true});
@ -127,20 +127,14 @@ this.TopSitesFeed = class TopSitesFeed {
}
/**
* isExperimentOnAndLinkFilteredSearch - is the experiment on and does a given hostname match the user's default search engine?
* shouldFilterSearchTile - is default filtering enabled and does a given hostname match the user's default search engine?
*
* @param {string} hostname a top site hostname, such as "amazon" or "foo"
* @returns {bool}
*/
isExperimentOnAndLinkFilteredSearch(hostname) {
if (!this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF]) {
return false;
}
// If TopSite Search Shortcuts is enabled we don't want to filter those sites out
if (this.store.getState().Prefs.values[SEARCH_SHORTCUTS_EXPERIMENT] && getSearchProvider(hostname)) {
return false;
}
if (SEARCH_FILTERS.includes(hostname) || hostname === this._currentSearchHostname) {
shouldFilterSearchTile(hostname) {
if (this.store.getState().Prefs.values[FILTER_DEFAULT_SEARCH_PREF] &&
(SEARCH_FILTERS.includes(hostname) || hostname === this._currentSearchHostname)) {
return true;
}
return false;
@ -227,7 +221,7 @@ this.TopSitesFeed = class TopSitesFeed {
});
for (let link of cache) {
const hostname = shortURL(link);
if (!this.isExperimentOnAndLinkFilteredSearch(hostname)) {
if (!this.shouldFilterSearchTile(hostname)) {
frecent.push({
...(searchShortcutsExperiment ? await this.topSiteToSearchTopSite(link) : link),
hostname,
@ -241,7 +235,7 @@ this.TopSitesFeed = class TopSitesFeed {
const searchProvider = getSearchProvider(shortURL(link));
if (NewTabUtils.blockedLinks.isBlocked({url: link.url})) {
continue;
} else if (this.isExperimentOnAndLinkFilteredSearch(link.hostname)) {
} else if (this.shouldFilterSearchTile(link.hostname)) {
continue;
// If we've previously blocked a search shortcut, remove the default top site
// that matches the hostname
@ -670,7 +664,7 @@ this.TopSitesFeed = class TopSitesFeed {
this.refreshDefaults(action.data.value);
break;
case ROWS_PREF:
case NO_DEFAULT_SEARCH_TILE_EXP_PREF:
case FILTER_DEFAULT_SEARCH_PREF:
case SEARCH_SHORTCUTS_SEARCH_ENGINES_PREF:
this.refresh({broadcast: true});
break;

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

Различия файлов скрыты, потому что одна или несколько строк слишком длинны

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

@ -9,9 +9,9 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
<script src="chrome://browser/content/contentSearchUI.js"></script>
<script src="chrome://browser/content/contentTheme.js"></script>
<script src="resource://activity-stream/vendor/react.js"></script>

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

@ -9,8 +9,8 @@
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="header-asrouter-container"></div>
<div id="header-asrouter-container" role="presentation"></div>
<div id="root"><!-- Regular React Rendering --></div>
<div id="footer-asrouter-container"></div>
<div id="footer-asrouter-container" role="presentation"></div>
</body>
</html>

Некоторые файлы не были показаны из-за слишком большого количества измененных файлов Показать больше