Bug 1486631 - Add CFR, search shortcut fixes, and bug fixes to Activity Stream r=Mardak,ursula

MozReview-Commit-ID: HZbYyg4FGwi

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
k88hudson 2018-08-28 20:29:50 +00:00
Родитель 8741a0d774
Коммит d21e43c53a
88 изменённых файлов: 1713 добавлений и 640 удалений

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

@ -63,6 +63,7 @@ for (const type of [
"PLACES_LINK_BLOCKED",
"PLACES_LINK_DELETED",
"PLACES_SAVED_TO_POCKET",
"POCKET_WAITING_FOR_SPOC",
"PREFS_INITIAL_VALUES",
"PREF_CHANGED",
"PREVIEW_REQUEST",

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

@ -37,7 +37,8 @@ const INITIAL_STATE = {
visible: false,
data: {}
},
Sections: []
Sections: [],
Pocket: {waitingForSpoc: true}
};
function App(prevState = INITIAL_STATE.App, action) {
@ -381,10 +382,19 @@ function Snippets(prevState = INITIAL_STATE.Snippets, action) {
}
}
function Pocket(prevState = INITIAL_STATE.Pocket, action) {
switch (action.type) {
case at.POCKET_WAITING_FOR_SPOC:
return {...prevState, waitingForSpoc: action.data};
default:
return prevState;
}
}
this.INITIAL_STATE = INITIAL_STATE;
this.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;
this.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;
this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections};
this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket};
const EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];

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

@ -1,7 +1,9 @@
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
import {addSnippetsSubscriber} from "content-src/lib/snippets";
import {ASRouterContent} from "content-src/asrouter/asrouter-content";
import {Base} from "content-src/components/Base/Base";
import {DetectUserSessionStart} from "content-src/lib/detect-user-session-start";
import {enableASRouterContent} from "content-src/lib/asroutercontent";
import {initStore} from "content-src/lib/init-store";
import {Provider} from "react-redux";
import React from "react";
@ -9,6 +11,7 @@ import ReactDOM from "react-dom";
import {reducers} from "common/Reducers.jsm";
const store = initStore(reducers, global.gActivityStreamPrerenderedState);
const asrouterContent = new ASRouterContent();
new DetectUserSessionStart(store).sendEventOrAddListener();
@ -27,4 +30,5 @@ ReactDOM.hydrate(<Provider store={store}>
strings={global.gActivityStreamStrings} />
</Provider>, document.getElementById("root"));
enableASRouterContent(store, asrouterContent);
addSnippetsSubscriber(store);

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

@ -5,7 +5,25 @@
Name | Used for | Type | Example value
--- | --- | --- | ---
`whitelistHosts` | Whitelist a host in order to fetch messages from its endpoint | `[String]` | `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]`
`snippetsUrl` | The main remote endpoint that serves all snippet messages | `String` | `https://activity-stream-icons.services.mozilla.com/v1/messages.json.br`
`messageProviders` | Message provider options | `Object` | [see below](#message-providers)
### Message providers
```json
[
{
"id" : "onboarding",
"type" : "local",
"localProvider" : "OnboardingMessageProvider"
},
{
"type" : "remote",
"url" : "https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json",
"updateCycleInMs" : 14400000,
"id" : "snippets"
}
]
```
## Admin Interface

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

@ -176,6 +176,11 @@ export class ASRouterUISurface extends React.PureComponent {
this.setState({message: {}});
}
break;
case "CLEAR_PROVIDER":
if (action.data.id === this.state.message.provider) {
this.setState({message: {}});
}
break;
case "CLEAR_BUNDLE":
if (this.state.bundle.bundle) {
this.setState({bundle: {}});

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

@ -4,8 +4,6 @@ Field name | Type | Required | Description | Example / Note
--- | --- | --- | --- | ---
`id` | `string` | Yes | A unique identifier for the message that should not conflict with any other previous message | `ONBOARDING_1`
`template` | `string` | Yes | An id matching an existing Activity Stream Router template | [See example](https://github.com/mozilla/activity-stream/blob/33669c67c2269078a6d3d6d324fb48175d98f634/system-addon/content-src/message-center/templates/SimpleSnippet.jsx)
`publish_start` | `date` | No | When to start showing the message | `1524474850876`
`publish_end` | `date` | No | When to stop showing the message | `1524474850876`
`content` | `object` | Yes | An object containing all variables/props to be rendered in the template. Subset of allowed tags detailed below. | [See example below](#html-subset)
`bundled` | `integer` | No | The number of messages of the same template this one should be shown with | [See example below](#a-bundled-message-example)
`order` | `integer` | No | If bundled with other messages of the same template, which order should this one be placed in? Defaults to 0 if no order is desired | [See example below](#a-bundled-message-example)
@ -96,6 +94,7 @@ Name | Type | Example value | Description
`isDefaultBrowser` | `Boolean` or `null` | Is Firefox the user's default browser? If we could not determine the default browser, this value is `null`
`profileAgeCreated` | Number | `1522843725924` | Profile creation timestamp
`profileAgeReset` | `Number` or `undefined` | `1522843725924` | When (if) the profile was reset
`currentDate` | `Date` | `Date 2018-08-22T15:48:04.100Z` | Date object of current time in UTC
`searchEngines` | `Object` | [example below](#searchengines-example) | Information about the current and available search engines
`browserSettings.attribution` | `Object` or `undefined` | [example below](#attribution-example) | Attribution for the source of of where the browser was downloaded.
@ -170,4 +169,11 @@ Examples:
// targeting addon information
"targeting": "addonsInfo.addons['activity-stream@mozilla.org'].name == 'Activity Stream'"
}
{
"id": "7866",
"content": {...},
// targeting based on time
"targeting": "currentDate > '2018-08-08'|date"
}
```

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

@ -0,0 +1,147 @@
{
"title": "ExtensionDoorhanger",
"description": "A template with a heading, addon icon, title and description. No markup allowed.",
"version": "1.0.0",
"type": "object",
"definitions": {
"plainText": {
"description": "Plain text (no HTML allowed)",
"type": "string"
},
"linkUrl": {
"description": "Target for links or buttons",
"type": "string",
"format": "uri"
}
},
"properties": {
"notification_text": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Text for location bar chiclet."}
]
},
"info_icon": {
"type": "object",
"properties": {
"label": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Text for button tooltip used to provider information about the doorhanger."}
]
},
"sumo_path": {
"type": "string",
"description": "Last part of the path in the URL to the support page with the information about the doorhanger.",
"examples": ["extensionpromotions", "extensionrecommendations"]
}
}
},
"heading_text": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Doorhanger heading describing its purpose."}
]
},
"addon": {
"type": "object",
"properties": {
"name": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Addon name"}
]
},
"author": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Addon author"}
]
},
"icon": {
"allOf": [
{"$ref": "#/definitions/linkUrl"},
{"description": "Addon icon"}
]
},
"amo_url": {
"allOf": [
{"$ref": "#/definitions/linkUrl"},
{"description": "Link that offers more information related to the addon."}
]
}
}
},
"text": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Description for the addon."}
]
},
"buttons": {
"type": "object",
"properties": {
"primary": {
"type": "object",
"properties": {
"label": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Text for the primary button of the doorhanger."}
]
},
"accessKey": {
"type": "string",
"description": "A single character to be used as a shortcut key for the primary button. This should be one of the characters that appears in the button label."
},
"action": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Action dispatched by the button."
},
"data": {
"properties": {
"url": {
"allOf": [
{"$ref": "#/definitions/linkUrl"},
{"description": "URL used in combination with the primary action dispatched."}
]
}
}
}
}
}
},
"secondary": {
"type": "object",
"properties": {
"label": {
"allOf": [
{"$ref": "#/definitions/plainText"},
{"description": "Text for the secondary button of the doorhanger."}
]
},
"accessKey": {
"type": "string",
"description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label."
},
"action": {
"type": "object",
"properties": {
"type": {
"type": "string",
"description": "Action dispatched by the button."
}
}
}
}
}
}
}
}
},
"additionalProperties": false,
"required": ["notification_text", "heading_text", "addon", "text", "buttons"]
}

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

@ -9,7 +9,13 @@ class OnboardingCard extends React.PureComponent {
onClick() {
const {props} = this;
props.sendUserActionTelemetry({event: "CLICK_BUTTON", message_id: props.id, id: props.UISurface});
const ping = {
event: "CLICK_BUTTON",
message_id: props.id,
id: props.UISurface,
includeClientID: true
};
props.sendUserActionTelemetry(ping);
props.onAction(props.content.button_action);
}

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

@ -53,8 +53,8 @@ export class ASRouterAdmin extends React.PureComponent {
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.blockList.includes(msg.id);
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
const isBlocked = this.state.messageBlockList.includes(msg.id);
const impressions = this.state.messageImpressions[msg.id] ? this.state.impressions[msg.id].length : 0;
let itemClassName = "message-item";
if (isCurrent) { itemClassName += " current"; }
@ -84,10 +84,18 @@ export class ASRouterAdmin extends React.PureComponent {
renderProviders() {
return (<table><tbody>
{this.state.providers.map((provider, i) => (<tr className="message-item" key={i}>
<td>{provider.id}</td>
<td>{provider.type === "remote" ? <a target="_blank" href={provider.url}>{provider.url}</a> : "(local)"}</td>
</tr>))}
{this.state.providers.map((provider, i) => {
let label = "(local)";
if (provider.type === "remote") {
label = <a target="_blank" href={provider.url}>{provider.url}</a>;
} else if (provider.type === "remote-settings") {
label = `${provider.bucket} (Remote Settings)`;
}
return (<tr className="message-item" key={i}>
<td>{provider.id}</td>
<td>{label}</td>
</tr>);
})}
</tbody></table>);
}

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

@ -114,7 +114,7 @@ export class _Base extends React.PureComponent {
const {initialized} = App;
const prefs = props.Prefs.values;
if ((prefs.asrouterExperimentEnabled || prefs.asrouterOnboardingCohort > 0) && window.location.hash === "#asrouter") {
if (prefs.asrouterExperimentEnabled && window.location.hash === "#asrouter") {
return (<ASRouterAdmin />);
}
@ -124,7 +124,12 @@ export class _Base extends React.PureComponent {
// Until we can delete the existing onboarding tour, just hide the onboarding button when users are in
// the new simplified onboarding experiment. CSS hacks ftw
if (prefs.asrouterOnboardingCohort > 0) {
let isOnboardingEnabled = false;
try {
isOnboardingEnabled = JSON.parse(prefs["asrouter.messageProviders"]).find(i => i.id === "onboarding").enabled;
} catch (e) {}
if (isOnboardingEnabled) {
global.document.body.classList.add("hide-onboarding");
}

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

@ -129,6 +129,7 @@ export class Section extends React.PureComponent {
pref, privacyNoticeURL, isFirst, isLast
} = this.props;
const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
const {numRows} = this;
const maxCards = maxCardsPerRow * numRows;
@ -152,7 +153,13 @@ export class Section extends React.PureComponent {
// On narrow viewports, we only show 3 cards per row. We'll mark the rest as
// .hide-for-narrow to hide in CSS via @media query.
const className = (i >= maxCardsOnNarrow) ? "hide-for-narrow" : "";
cards.push(link ? (
let usePlaceholder = !link;
// If we are in the third card and waiting for spoc,
// use the placeholder.
if (!usePlaceholder && i === 2 && waitingForSpoc) {
usePlaceholder = true;
}
cards.push(!usePlaceholder ? (
<Card key={i}
index={i}
className={className}
@ -218,7 +225,7 @@ Section.defaultProps = {
title: ""
};
export const SectionIntl = connect(state => ({Prefs: state.Prefs}))(injectIntl(Section));
export const SectionIntl = connect(state => ({Prefs: state.Prefs, Pocket: state.Pocket}))(injectIntl(Section));
export class _Sections extends React.PureComponent {
renderSections() {

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

@ -26,8 +26,8 @@ export class _StartupOverlay extends React.PureComponent {
if (this.props.fxa_endpoint && !this.didFetch) {
try {
this.didFetch = true;
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?entrypoint=
activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email`);
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}`);
if (response.status === 200) {
const {flowId, flowBeginTime} = await response.json();
this.setState({flowId, flowBeginTime});

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

@ -8,6 +8,7 @@ $default-icon-wrapper-size: 42px;
$default-icon-size: 32px;
$default-icon-offset: 6px;
$half-base-gutter: $base-gutter / 2;
$hover-transition-duration: 150ms;
.top-sites {
// Take back the margin from the bottom row of vertical spacing as well as the
@ -136,6 +137,7 @@ $half-base-gutter: $base-gutter / 2;
font-weight: 200;
justify-content: center;
text-transform: uppercase; // sass-lint:disable-line no-disallowed-properties
transition: box-shadow $hover-transition-duration;
&::before {
content: attr(data-fallback);
@ -202,10 +204,24 @@ $half-base-gutter: $base-gutter / 2;
background-image: url('#{$image-path}glyph-search-16.svg');
background-size: 26px;
background-color: $blue-60;
border-radius: 42px;
border-radius: $default-icon-wrapper-size;
-moz-context-properties: fill;
fill: $white;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow);
box-shadow: var(--newtab-card-shadow);
transition-duration: $hover-transition-duration;
transition-property: background-size, bottom, inset-inline-end, height, width;
}
&:hover .search-topsite {
$hover-icon-wrapper-size: $default-icon-wrapper-size + 4;
$hover-icon-offset: -$default-icon-offset - 3;
background-size: 28px;
border-radius: $hover-icon-wrapper-size;
bottom: $hover-icon-offset;
height: $hover-icon-wrapper-size;
inset-inline-end: $hover-icon-offset;
width: $hover-icon-wrapper-size;
}
// We want all search shortcuts to have a white background in case they have transparency.
@ -576,13 +592,6 @@ $half-base-gutter: $base-gutter / 2;
}
}
// when unselected, higlight the tile on hover
[type='checkbox']:not(:checked) + label {
.tile:hover {
@include fade-in;
}
}
// checkmark changes
[type='checkbox']:not(:checked) + label::after {
opacity: 0;
@ -592,11 +601,6 @@ $half-base-gutter: $base-gutter / 2;
opacity: 1;
}
// hover
[type='checkbox'] + label:hover::before {
border: 1px solid var(--newtab-link-primary-color);
}
// accessibility
[type='checkbox']:checked:focus + label::before,
[type='checkbox']:not(:checked):focus + label::before {

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

@ -0,0 +1,13 @@
export function enableASRouterContent(store, asrouterContent) {
// Enable asrouter content
store.subscribe(() => {
const state = store.getState();
if (state.Prefs.values.asrouterExperimentEnabled && !asrouterContent.initialized) {
asrouterContent.init();
} else if (!state.Prefs.values.asrouterExperimentEnabled && asrouterContent.initialized) {
asrouterContent.uninit();
}
});
// Return this for testing purposes
return {asrouterContent};
}

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

@ -7,7 +7,6 @@ const SNIPPETS_ENABLED_EVENT = "Snippets:Enabled";
const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
import {ASRouterContent} from "content-src/asrouter/asrouter-content";
/**
* SnippetsMap - A utility for cacheing values related to the snippet. It has
@ -377,13 +376,16 @@ export class SnippetsProvider {
*/
export function addSnippetsSubscriber(store) {
const snippets = new SnippetsProvider(store.dispatch);
const asrouterContent = new ASRouterContent();
let initializing = false;
store.subscribe(async () => {
const state = store.getState();
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort > 0;
let snippetsEnabled = false;
try {
snippetsEnabled = JSON.parse(state.Prefs.values["asrouter.messageProviders"]).find(i => i.id === "snippets").enabled;
} catch (e) {}
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && snippetsEnabled;
// state.Prefs.values["feeds.snippets"]: Should snippets be shown?
// state.Snippets.initialized Is the snippets data initialized?
// snippets.initialized: Is SnippetsProvider currently initialised?
@ -407,22 +409,8 @@ export function addSnippetsSubscriber(store) {
) {
snippets.uninit();
}
// Turn on AS Router snippets if the experiment is enabled and the snippets pref is on;
// otherwise, turn it off.
if (
(state.Prefs.values.asrouterExperimentEnabled || state.Prefs.values.asrouterOnboardingCohort > 0) &&
state.Prefs.values["feeds.snippets"] &&
!asrouterContent.initialized) {
asrouterContent.init();
} else if (
((!state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort === 0) || !state.Prefs.values["feeds.snippets"]) &&
asrouterContent.initialized
) {
asrouterContent.uninit();
}
});
// These values are returned for testing purposes
return {snippets, asrouterContent};
// Returned for testing purposes
return {snippets};
}

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

@ -157,7 +157,7 @@ $textbox-shadow-size: 4px;
position: absolute;
top: -($context-menu-button-size / 2);
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: $context-menu-button-size;

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

@ -505,7 +505,7 @@ main {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px; }
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
@ -524,7 +524,8 @@ main {
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase; }
text-transform: uppercase;
transition: box-shadow 150ms; }
.top-site-outer .tile::before {
content: attr(data-fallback); }
.top-site-outer .screenshot {
@ -576,7 +577,16 @@ main {
border-radius: 42px;
-moz-context-properties: fill;
fill: #FFF;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
box-shadow: var(--newtab-card-shadow);
transition-duration: 150ms;
transition-property: background-size, bottom, inset-inline-end, height, width; }
.top-site-outer:hover .search-topsite {
background-size: 28px;
border-radius: 46px;
bottom: -9px;
height: 46px;
inset-inline-end: -9px;
width: 46px; }
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF; }
.top-site-outer .title {
@ -822,19 +832,12 @@ main {
.topsite-form [type='checkbox']:checked + label .tile {
box-shadow: 0 0 0 2px var(--newtab-link-primary-color); }
.topsite-form [type='checkbox']:not(:checked) + label .tile:hover {
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), 0 0 0 5px var(--newtab-card-active-outline-color);
transition: box-shadow 150ms; }
.topsite-form [type='checkbox']:not(:checked) + label::after {
opacity: 0; }
.topsite-form [type='checkbox']:checked + label::after {
opacity: 1; }
.topsite-form [type='checkbox'] + label:hover::before {
border: 1px solid var(--newtab-link-primary-color); }
.topsite-form [type='checkbox']:checked:focus + label::before,
.topsite-form [type='checkbox']:not(:checked):focus + label::before {
border: 1px dotted var(--newtab-link-primary-color); }
@ -1458,7 +1461,7 @@ a.firstrun-link {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px; }
.card-outer .context-menu-button:-moz-any(:active, :focus) {

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

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

@ -508,7 +508,7 @@ main {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px; }
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
@ -527,7 +527,8 @@ main {
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase; }
text-transform: uppercase;
transition: box-shadow 150ms; }
.top-site-outer .tile::before {
content: attr(data-fallback); }
.top-site-outer .screenshot {
@ -579,7 +580,16 @@ main {
border-radius: 42px;
-moz-context-properties: fill;
fill: #FFF;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
box-shadow: var(--newtab-card-shadow);
transition-duration: 150ms;
transition-property: background-size, bottom, inset-inline-end, height, width; }
.top-site-outer:hover .search-topsite {
background-size: 28px;
border-radius: 46px;
bottom: -9px;
height: 46px;
inset-inline-end: -9px;
width: 46px; }
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF; }
.top-site-outer .title {
@ -825,19 +835,12 @@ main {
.topsite-form [type='checkbox']:checked + label .tile {
box-shadow: 0 0 0 2px var(--newtab-link-primary-color); }
.topsite-form [type='checkbox']:not(:checked) + label .tile:hover {
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), 0 0 0 5px var(--newtab-card-active-outline-color);
transition: box-shadow 150ms; }
.topsite-form [type='checkbox']:not(:checked) + label::after {
opacity: 0; }
.topsite-form [type='checkbox']:checked + label::after {
opacity: 1; }
.topsite-form [type='checkbox'] + label:hover::before {
border: 1px solid var(--newtab-link-primary-color); }
.topsite-form [type='checkbox']:checked:focus + label::before,
.topsite-form [type='checkbox']:not(:checked):focus + label::before {
border: 1px dotted var(--newtab-link-primary-color); }
@ -1461,7 +1464,7 @@ a.firstrun-link {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px; }
.card-outer .context-menu-button:-moz-any(:active, :focus) {

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

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

@ -505,7 +505,7 @@ main {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px; }
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
@ -524,7 +524,8 @@ main {
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase; }
text-transform: uppercase;
transition: box-shadow 150ms; }
.top-site-outer .tile::before {
content: attr(data-fallback); }
.top-site-outer .screenshot {
@ -576,7 +577,16 @@ main {
border-radius: 42px;
-moz-context-properties: fill;
fill: #FFF;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
box-shadow: var(--newtab-card-shadow);
transition-duration: 150ms;
transition-property: background-size, bottom, inset-inline-end, height, width; }
.top-site-outer:hover .search-topsite {
background-size: 28px;
border-radius: 46px;
bottom: -9px;
height: 46px;
inset-inline-end: -9px;
width: 46px; }
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF; }
.top-site-outer .title {
@ -822,19 +832,12 @@ main {
.topsite-form [type='checkbox']:checked + label .tile {
box-shadow: 0 0 0 2px var(--newtab-link-primary-color); }
.topsite-form [type='checkbox']:not(:checked) + label .tile:hover {
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), 0 0 0 5px var(--newtab-card-active-outline-color);
transition: box-shadow 150ms; }
.topsite-form [type='checkbox']:not(:checked) + label::after {
opacity: 0; }
.topsite-form [type='checkbox']:checked + label::after {
opacity: 1; }
.topsite-form [type='checkbox'] + label:hover::before {
border: 1px solid var(--newtab-link-primary-color); }
.topsite-form [type='checkbox']:checked:focus + label::before,
.topsite-form [type='checkbox']:not(:checked):focus + label::before {
border: 1px dotted var(--newtab-link-primary-color); }
@ -1458,7 +1461,7 @@ a.firstrun-link {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 200ms;
transition-duration: 150ms;
transition-property: transform, opacity;
width: 27px; }
.card-outer .context-menu-button:-moz-any(:active, :focus) {

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

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

@ -92,16 +92,18 @@
__webpack_require__.r(__webpack_exports__);
/* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
/* harmony import */ var content_src_lib_snippets__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
/* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(12);
/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(38);
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7);
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(16);
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(10);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(42);
/* harmony import */ var content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
/* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12);
/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(38);
/* harmony import */ var content_src_lib_asroutercontent__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(39);
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7);
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(16);
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_7__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(5);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(10);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_9__);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(43);
@ -112,9 +114,12 @@ __webpack_require__.r(__webpack_exports__);
const store = Object(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_4__["initStore"])(common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__["reducers"], global.gActivityStreamPrerenderedState);
new content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_3__["DetectUserSessionStart"](store).sendEventOrAddListener();
const store = Object(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_6__["initStore"])(common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_10__["reducers"], global.gActivityStreamPrerenderedState);
const asrouterContent = new content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__["ASRouterContent"]();
new content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_4__["DetectUserSessionStart"](store).sendEventOrAddListener();
// If we are starting in a prerendered state, we must wait until the first render
// to request state rehydration (see Base.jsx). If we are NOT in a prerendered state,
@ -123,16 +128,17 @@ if (!global.gActivityStreamPrerenderedState) {
store.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_STATE_REQUEST }));
}
react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
react_redux__WEBPACK_IMPORTED_MODULE_5__["Provider"],
react_dom__WEBPACK_IMPORTED_MODULE_9___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
react_redux__WEBPACK_IMPORTED_MODULE_7__["Provider"],
{ store: store },
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_2__["Base"], {
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__["Base"], {
isFirstrun: global.document.location.href === "about:welcome",
isPrerendered: !!global.gActivityStreamPrerenderedState,
locale: global.document.documentElement.lang,
strings: global.gActivityStreamStrings })
), document.getElementById("root"));
Object(content_src_lib_asroutercontent__WEBPACK_IMPORTED_MODULE_5__["enableASRouterContent"])(store, asrouterContent);
Object(content_src_lib_snippets__WEBPACK_IMPORTED_MODULE_1__["addSnippetsSubscriber"])(store);
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
@ -205,7 +211,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_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_ONBOARDING", "DOWNLOAD_CHANGED", "FILL_SEARCH_TERM", "INIT", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "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", "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", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "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_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_ONBOARDING", "DOWNLOAD_CHANGED", "FILL_SEARCH_TERM", "INIT", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "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_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", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "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;
}
@ -483,7 +489,6 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsProvider", function() { return SnippetsProvider; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "addSnippetsSubscriber", function() { return addSnippetsSubscriber; });
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
/* harmony import */ var content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
const DATABASE_NAME = "snippets_db";
@ -496,7 +501,6 @@ const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
/**
* SnippetsMap - A utility for cacheing values related to the snippet. It has
* the same interface as a Map, but is optionally backed by
@ -876,13 +880,18 @@ class SnippetsProvider {
*/
function addSnippetsSubscriber(store) {
const snippets = new SnippetsProvider(store.dispatch);
const asrouterContent = new content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_1__["ASRouterContent"]();
let initializing = false;
store.subscribe(_asyncToGenerator(function* () {
const state = store.getState();
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort > 0;
let snippetsEnabled = false;
try {
snippetsEnabled = JSON.parse(state.Prefs.values["asrouter.messageProviders"]).find(function (i) {
return i.id === "snippets";
}).enabled;
} catch (e) {}
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && snippetsEnabled;
// state.Prefs.values["feeds.snippets"]: Should snippets be shown?
// state.Snippets.initialized Is the snippets data initialized?
// snippets.initialized: Is SnippetsProvider currently initialised?
@ -897,18 +906,10 @@ function addSnippetsSubscriber(store) {
} else if ((state.Prefs.values["feeds.snippets"] === false || state.Prefs.values.disableSnippets === true) && snippets.initialized) {
snippets.uninit();
}
// Turn on AS Router snippets if the experiment is enabled and the snippets pref is on;
// otherwise, turn it off.
if ((state.Prefs.values.asrouterExperimentEnabled || state.Prefs.values.asrouterOnboardingCohort > 0) && state.Prefs.values["feeds.snippets"] && !asrouterContent.initialized) {
asrouterContent.init();
} else if ((!state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort === 0 || !state.Prefs.values["feeds.snippets"]) && asrouterContent.initialized) {
asrouterContent.uninit();
}
}));
// These values are returned for testing purposes
return { snippets, asrouterContent };
// Returned for testing purposes
return { snippets };
}
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
@ -922,18 +923,18 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertLinks", function() { return convertLinks; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUISurface", function() { return ASRouterUISurface; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterContent", function() { return ASRouterContent; });
/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(40);
/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(41);
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
/* harmony import */ var _components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
/* harmony import */ var fluent__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(39);
/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(43);
/* harmony import */ var fluent__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40);
/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(44);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(10);
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
/* harmony import */ var _template_utils__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(11);
/* harmony import */ var _templates_SimpleSnippet_SimpleSnippet__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(41);
/* harmony import */ var _templates_SimpleSnippet_SimpleSnippet__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(42);
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
@ -1117,6 +1118,11 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
this.setState({ message: {} });
}
break;
case "CLEAR_PROVIDER":
if (action.data.id === this.state.message.provider) {
this.setState({ message: {} });
}
break;
case "CLEAR_BUNDLE":
if (this.state.bundle.bundle) {
this.setState({ bundle: {} });
@ -1631,7 +1637,7 @@ class _Base extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
const { initialized } = App;
const prefs = props.Prefs.values;
if ((prefs.asrouterExperimentEnabled || prefs.asrouterOnboardingCohort > 0) && window.location.hash === "#asrouter") {
if (prefs.asrouterExperimentEnabled && window.location.hash === "#asrouter") {
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_2__["ASRouterAdmin"], null);
}
@ -1641,7 +1647,12 @@ class _Base extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
// Until we can delete the existing onboarding tour, just hide the onboarding button when users are in
// the new simplified onboarding experiment. CSS hacks ftw
if (prefs.asrouterOnboardingCohort > 0) {
let isOnboardingEnabled = false;
try {
isOnboardingEnabled = JSON.parse(prefs["asrouter.messageProviders"]).find(i => i.id === "onboarding").enabled;
} catch (e) {}
if (isOnboardingEnabled) {
global.document.body.classList.add("hide-onboarding");
}
@ -1808,8 +1819,8 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.blockList.includes(msg.id);
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
const isBlocked = this.state.messageBlockList.includes(msg.id);
const impressions = this.state.messageImpressions[msg.id] ? this.state.impressions[msg.id].length : 0;
let itemClassName = "message-item";
if (isCurrent) {
@ -1885,24 +1896,32 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"tbody",
null,
this.state.providers.map((provider, i) => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"tr",
{ className: "message-item", key: i },
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"td",
null,
provider.id
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"td",
null,
provider.type === "remote" ? react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
this.state.providers.map((provider, i) => {
let label = "(local)";
if (provider.type === "remote") {
label = react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"a",
{ target: "_blank", href: provider.url },
provider.url
) : "(local)"
)
))
);
} else if (provider.type === "remote-settings") {
label = `${provider.bucket} (Remote Settings)`;
}
return react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"tr",
{ className: "message-item", key: i },
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"td",
null,
provider.id
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"td",
null,
label
)
);
})
)
);
}
@ -2461,7 +2480,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionIntl", function() { return SectionIntl; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Sections", function() { return _Sections; });
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Sections", function() { return Sections; });
/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(44);
/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(45);
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2);
@ -2607,6 +2626,7 @@ class Section extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent
pref, privacyNoticeURL, isFirst, isLast
} = this.props;
const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
const { numRows } = this;
const maxCards = maxCardsPerRow * numRows;
@ -2629,7 +2649,13 @@ class Section extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent
// On narrow viewports, we only show 3 cards per row. We'll mark the rest as
// .hide-for-narrow to hide in CSS via @media query.
const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : "";
cards.push(link ? react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__["Card"], { key: i,
let usePlaceholder = !link;
// If we are in the third card and waiting for spoc,
// use the placeholder.
if (!usePlaceholder && i === 2 && waitingForSpoc) {
usePlaceholder = true;
}
cards.push(!usePlaceholder ? react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__["Card"], { key: i,
index: i,
className: className,
dispatch: dispatch,
@ -2696,7 +2722,7 @@ Section.defaultProps = {
title: ""
};
const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Prefs: state.Prefs }))(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(Section));
const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Prefs: state.Prefs, Pocket: state.Pocket }))(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(Section));
class _Sections extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
renderSections() {
@ -3996,8 +4022,8 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(35);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(42);
/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(45);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(43);
/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(46);
/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(36);
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
@ -4405,7 +4431,7 @@ __webpack_require__.r(__webpack_exports__);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5);
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(26);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(42);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(43);
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
@ -4977,8 +5003,8 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
if (_this.props.fxa_endpoint && !_this.didFetch) {
try {
_this.didFetch = true;
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?entrypoint=
activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email`);
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?${fxaParams}`);
if (response.status === 200) {
const { flowId, flowBeginTime } = yield response.json();
_this.setState({ flowId, flowBeginTime });
@ -5225,6 +5251,27 @@ class DetectUserSessionStart {
/* 39 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "enableASRouterContent", function() { return enableASRouterContent; });
function enableASRouterContent(store, asrouterContent) {
// Enable asrouter content
store.subscribe(() => {
const state = store.getState();
if (state.Prefs.values.asrouterExperimentEnabled && !asrouterContent.initialized) {
asrouterContent.init();
} else if (!state.Prefs.values.asrouterExperimentEnabled && asrouterContent.initialized) {
asrouterContent.uninit();
}
});
// Return this for testing purposes
return { asrouterContent };
}
/***/ }),
/* 40 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
// CONCATENATED MODULE: ./node_modules/fluent/src/parser.js
@ -7323,7 +7370,7 @@ function ftl(strings) {
/***/ }),
/* 40 */
/* 41 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -7336,7 +7383,7 @@ var external_PropTypes_ = __webpack_require__(6);
var external_PropTypes_default = /*#__PURE__*/__webpack_require__.n(external_PropTypes_);
// EXTERNAL MODULE: ./node_modules/fluent/src/index.js + 8 modules
var src = __webpack_require__(39);
var src = __webpack_require__(40);
// CONCATENATED MODULE: ./node_modules/fluent-react/src/localization.js
@ -7838,7 +7885,7 @@ localized_Localized.propTypes = {
/***/ }),
/* 41 */
/* 42 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -7990,7 +8037,7 @@ class SimpleSnippet_SimpleSnippet extends external_React_default.a.PureComponent
}
/***/ }),
/* 42 */
/* 43 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8076,7 +8123,8 @@ const INITIAL_STATE = {
visible: false,
data: {}
},
Sections: []
Sections: [],
Pocket: { waitingForSpoc: true }
};
@ -8422,10 +8470,19 @@ function Snippets(prevState = INITIAL_STATE.Snippets, action) {
}
}
var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections };
function Pocket(prevState = INITIAL_STATE.Pocket, action) {
switch (action.type) {
case Actions["actionTypes"].POCKET_WAITING_FOR_SPOC:
return Object.assign({}, prevState, { waitingForSpoc: action.data });
default:
return prevState;
}
}
var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket };
/***/ }),
/* 43 */
/* 44 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8496,7 +8553,13 @@ class OnboardingMessage_OnboardingCard extends external_React_default.a.PureComp
onClick() {
const { props } = this;
props.sendUserActionTelemetry({ event: "CLICK_BUTTON", message_id: props.id, id: props.UISurface });
const ping = {
event: "CLICK_BUTTON",
message_id: props.id,
id: props.UISurface,
includeClientID: true
};
props.sendUserActionTelemetry(ping);
props.onAction(props.content.button_action);
}
@ -8563,7 +8626,7 @@ class OnboardingMessage_OnboardingMessage extends external_React_default.a.PureC
}
/***/ }),
/* 44 */
/* 45 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8927,7 +8990,7 @@ const Card = Object(external_ReactRedux_["connect"])(state => ({ platform: state
const PlaceholderCard = props => external_React_default.a.createElement(Card, { placeholder: true, className: props.className });
/***/ }),
/* 45 */
/* 46 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";

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

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.6 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.4 KiB

Двоичные данные
browser/components/newtab/data/content/assets/cfr_fb_container.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 3.3 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.7 KiB

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 6.4 KiB

Двоичные данные
browser/components/newtab/data/content/assets/cfr_wiki_search.png Normal file

Двоичный файл не отображается.

После

Ширина:  |  Высота:  |  Размер: 1.2 KiB

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

@ -576,10 +576,11 @@ This reports the impression of Activity Stream Router.
This reports the user's interaction with Activity Stream Router.
#### Snippets interaction pings
```js
{
"client_id": "n/a",
"action": ["snippets_user_event" | "onboarding_user_event"],
"action": "snippets_user_event",
"addon_version": "20180710100040",
"impression_id": "{005deed0-e3e4-4c02-a041-17405fd703f6}",
"locale": "en-US",
@ -589,6 +590,20 @@ This reports the user's interaction with Activity Stream Router.
}
```
#### Onboarding interaction pings
```js
{
"client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
"action": "onboarding_user_event",
"addon_version": "20180710100040",
"impression_id": "n/a",
"locale": "en-US",
"source": "NEWTAB_FOOTER_BAR",
"message_id": "onboarding_message_1",
"event": "CLICK_BUTTION"
}
```
### Targeting error pings
This reports when an error has occurred when parsing/evaluating a JEXL targeting string in a message.

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

@ -142,7 +142,9 @@ module.exports = function(config) {
path.resolve("vendor"),
path.resolve("lib/ASRouterTargeting.jsm"),
path.resolve("lib/ASRouterTriggerListeners.jsm"),
path.resolve("lib/OnboardingMessageProvider.jsm")
path.resolve("lib/OnboardingMessageProvider.jsm"),
path.resolve("lib/CFRMessageProvider.jsm"),
path.resolve("lib/CFRPageActions.jsm")
]
}
]

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

@ -5,11 +5,16 @@
ChromeUtils.import("resource://gre/modules/Services.jsm");
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
ChromeUtils.import("resource:///modules/UITour.jsm");
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
XPCOMUtils.defineLazyModuleGetters(this, {
AddonManager: "resource://gre/modules/AddonManager.jsm",
UITour: "resource:///modules/UITour.jsm"
});
const {ASRouterActions: ra, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {CFRMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/CFRMessageProvider.jsm", {});
const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/OnboardingMessageProvider.jsm", {});
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
const {CFRPageActions} = ChromeUtils.import("resource://activity-stream/lib/CFRPageActions.jsm", {});
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
"resource://activity-stream/lib/ASRouterTargeting.jsm");
@ -19,6 +24,7 @@ ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
const MESSAGE_PROVIDER_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
// List of hosts for endpoints that serve router messages.
// Key is allowed host, value is a name for the endpoint host.
const DEFAULT_WHITELIST_HOSTS = {
@ -26,8 +32,10 @@ const DEFAULT_WHITELIST_HOSTS = {
"snippets-admin.mozilla.org": "preview"
};
const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
// Max possible impressions cap for any message
const MAX_MESSAGE_LIFETIME_CAP = 100;
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider};
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider};
const STARTPAGE_VERSION = "0.1.0";
const MessageLoaderUtils = {
@ -70,6 +78,29 @@ const MessageLoaderUtils = {
return remoteMessages;
},
/**
* _remoteSettingsLoader - Loads messages for a RemoteSettings provider
*
* @param {obj} provider An AS router provider
* @param {string} provider.bucket The name of the Remote Settings bucket
* @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched
*/
async _remoteSettingsLoader(provider) {
let messages = [];
if (provider.bucket) {
try {
messages = await MessageLoaderUtils._getRemoteSettingsMessages(provider.bucket);
} catch (e) {
Cu.reportError(e);
}
}
return messages;
},
_getRemoteSettingsMessages(bucket) {
return RemoteSettings(bucket).get({filters: {locale: Services.locale.getAppLocaleAsLangTag()}});
},
/**
* _getMessageLoader - return the right loading function given the provider's type
*
@ -80,6 +111,8 @@ const MessageLoaderUtils = {
switch (provider.type) {
case "remote":
return this._remoteLoader;
case "remote-settings":
return this._remoteSettingsLoader;
case "local":
default:
return this._localLoader;
@ -144,8 +177,10 @@ class _ASRouter {
this._state = {
lastMessageId: null,
providers: [],
blockList: [],
impressions: {},
messageBlockList: [],
providerBlockList: [],
messageImpressions: {},
providerImpressions: {},
messages: []
};
this._triggerHandler = this._triggerHandler.bind(this);
@ -171,7 +206,11 @@ class _ASRouter {
const providers = existingPreviewProvider ? [existingPreviewProvider] : [];
const providersJSON = Services.prefs.getStringPref(this._messageProviderPref, "");
try {
JSON.parse(providersJSON).forEach(provider => providers.push(provider));
JSON.parse(providersJSON).forEach(provider => {
if (provider.enabled) {
providers.push(provider);
}
});
} catch (e) {
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
}
@ -283,10 +322,13 @@ class _ASRouter {
this._storage = storage;
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
this.dispatchToAS = dispatchToAS;
this.dispatch = this.dispatch.bind(this);
const blockList = await this._storage.get("blockList") || [];
const impressions = await this._storage.get("impressions") || {};
await this.setState({blockList, impressions});
const messageBlockList = await this._storage.get("messageBlockList") || [];
const providerBlockList = await this._storage.get("providerBlockList") || [];
const messageImpressions = await this._storage.get("messageImpressions") || {};
const providerImpressions = await this._storage.get("providerImpressions") || {};
await this.setState({messageBlockList, providerBlockList, messageImpressions, providerImpressions});
this._updateMessageProviders();
await this.loadMessagesFromAllProviders();
@ -336,18 +378,57 @@ class _ASRouter {
}
}
_findMessage(messages, target, trigger) {
const {impressions} = this.state;
_findMessage(candidateMessages, trigger) {
const messages = candidateMessages.filter(m => this.isBelowFrequencyCaps(m));
// Find a message that matches the targeting context as well as the trigger context (if one is provided)
// If no trigger is provided, we should find a message WITHOUT a trigger property defined.
return ASRouterTargeting.findMatchingMessage({messages, impressions, trigger, onError: this._handleTargetingError});
return ASRouterTargeting.findMatchingMessage({messages, trigger, onError: this._handleTargetingError});
}
_orderBundle(bundle) {
return bundle.sort((a, b) => a.order - b.order);
}
// Work out if a message can be shown based on its and its provider's frequency caps.
isBelowFrequencyCaps(message) {
const {providers, messageImpressions, providerImpressions} = this.state;
const provider = providers.find(p => p.id === message.provider);
const impressionsForMessage = messageImpressions[message.id];
const impressionsForProvider = providerImpressions[message.provider];
return (this._isBelowItemFrequencyCap(message, impressionsForMessage, MAX_MESSAGE_LIFETIME_CAP) &&
this._isBelowItemFrequencyCap(provider, impressionsForProvider));
}
// Helper for isBelowFrecencyCaps - work out if the frequency cap for the given
// item has been exceeded or not
_isBelowItemFrequencyCap(item, impressions, maxLifetimeCap = Infinity) {
if (item && item.frequency && impressions && impressions.length) {
if (
item.frequency.lifetime &&
impressions.length >= Math.min(item.frequency.lifetime, maxLifetimeCap)
) {
return false;
}
if (item.frequency.custom) {
const now = Date.now();
for (const setting of item.frequency.custom) {
let {period} = setting;
if (period === "daily") {
period = ONE_DAY_IN_MS;
}
const impressionsInPeriod = impressions.filter(t => (now - t) < period);
if (impressionsInPeriod.length >= setting.cap) {
return false;
}
}
}
}
return true;
}
async _getBundledMessages(originalMessage, target, trigger, force = false) {
let result = [{content: originalMessage.content, id: originalMessage.id, order: originalMessage.order || 0}];
@ -367,7 +448,7 @@ class _ASRouter {
} else {
while (bundledMessagesOfSameTemplate.length) {
// Find a message that matches the targeting context - or break if there are no matching messages
const message = await this._findMessage(bundledMessagesOfSameTemplate, target, trigger);
const message = await this._findMessage(bundledMessagesOfSameTemplate, trigger);
if (!message) {
/* istanbul ignore next */ // Code coverage in mochitests
break;
@ -393,40 +474,61 @@ class _ASRouter {
_getUnblockedMessages() {
let {state} = this;
return state.messages.filter(item => !state.blockList.includes(item.id));
return state.messages.filter(item =>
!state.messageBlockList.includes(item.id) &&
!state.providerBlockList.includes(item.provider)
);
}
async _sendMessageToTarget(message, target, trigger, force = false) {
let bundledMessages;
// If this message needs to be bundled with other messages of the same template, find them and bundle them together
if (message && message.bundled) {
bundledMessages = await this._getBundledMessages(message, target, trigger, force);
}
if (message && !message.bundled) {
// If we only need to send 1 message, send the message
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
} else if (bundledMessages) {
// If the message we want is bundled with other messages, send the entire bundle
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: bundledMessages});
// No message is available, so send CLEAR_ALL.
if (!message) {
try {
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
} catch (e) {}
// For bundled messages, look for the rest of the bundle or else send CLEAR_ALL
} else if (message.bundled) {
const bundledMessages = await this._getBundledMessages(message, target, trigger, force);
const action = bundledMessages ? {type: "SET_BUNDLED_MESSAGES", data: bundledMessages} : {type: "CLEAR_ALL"};
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
// CFR doorhanger
} else if (message.template === "cfr_doorhanger") {
CFRPageActions.addRecommendation(target, trigger.param, message, this.dispatch, force);
// New tab single messages
} else {
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
}
}
async addImpression(message) {
// Don't store impressions for messages that don't include any limits on frequency
if (!message.frequency) {
return;
const provider = this.state.providers.find(p => p.id === message.provider);
// We only need to store impressions for messages that have frequency, or
// that have providers that have frequency
if (message.frequency || (provider && provider.frequency)) {
const time = Date.now();
await this.setState(state => {
const messageImpressions = this._addImpressionForItem(state, message, "messageImpressions", time);
const providerImpressions = this._addImpressionForItem(state, provider, "providerImpressions", time);
return {messageImpressions, providerImpressions};
});
}
await this.setState(state => {
// The destructuring here is to avoid mutating existing objects in state as in redux
// (see https://redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)
const impressions = {...state.impressions};
impressions[message.id] = impressions[message.id] ? [...impressions[message.id]] : [];
impressions[message.id].push(Date.now());
this._storage.set("impressions", impressions);
return {impressions};
});
}
// Helper for addImpression - calculate the updated impressions object for the given
// item, then store it and return it
_addImpressionForItem(state, item, impressionsString, time) {
// The destructuring here is to avoid mutating existing objects in state as in redux
// (see https://redux.js.org/recipes/structuring-reducers/prerequisite-concepts#immutable-data-management)
const impressions = {...state[impressionsString]};
if (item.frequency) {
impressions[item.id] = impressions[item.id] ? [...impressions[item.id]] : [];
impressions[item.id].push(time);
this._storage.set(impressionsString, impressions);
}
return impressions;
}
/**
@ -447,41 +549,51 @@ class _ASRouter {
/**
* cleanupImpressions - this function cleans up obsolete impressions whenever
* messages are refreshed or fetched. It will likely need to be more sophisticated in the future,
* but the current behaviour for when impressions are cleared is as follows:
* but the current behaviour for when both message impressions and provider impressions are
* cleared is as follows (where `item` is either `message` or `provider`):
*
* 1. If the message id for a list of impressions no longer exists in state.messages, it will be cleared.
* 2. If the message has time-bound frequency caps but no lifetime cap, any impressions older
* 1. If the item id for a list of item impressions no longer exists in the ASRouter state, it
* will be cleared.
* 2. If the item has time-bound frequency caps but no lifetime cap, any item impressions older
* than the longest time period will be cleared.
*/
async cleanupImpressions() {
await this.setState(state => {
const impressions = {...state.impressions};
let needsUpdate = false;
Object.keys(impressions).forEach(id => {
const [message] = state.messages.filter(msg => msg.id === id);
// Don't keep impressions for messages that no longer exist
if (!message || !message.frequency || !Array.isArray(impressions[id])) {
delete impressions[id];
needsUpdate = true;
return;
}
if (!impressions[id].length) {
return;
}
// If we don't want to store impressions older than the longest period
if (message.frequency.custom && !message.frequency.lifetime) {
const now = Date.now();
impressions[id] = impressions[id].filter(t => (now - t) < this.getLongestPeriod(message));
needsUpdate = true;
}
});
if (needsUpdate) {
this._storage.set("impressions", impressions);
}
return {impressions};
const messageImpressions = this._cleanupImpressionsForItems(state, state.messages, "messageImpressions");
const providerImpressions = this._cleanupImpressionsForItems(state, state.providers, "providerImpressions");
return {messageImpressions, providerImpressions};
});
}
// Helper for cleanupImpressions - calculate the updated impressions object for
// the given items, then store it and return it
_cleanupImpressionsForItems(state, items, impressionsString) {
const impressions = {...state[impressionsString]};
let needsUpdate = false;
Object.keys(impressions).forEach(id => {
const [item] = items.filter(x => x.id === id);
// Don't keep impressions for items that no longer exist
if (!item || !item.frequency || !Array.isArray(impressions[id])) {
delete impressions[id];
needsUpdate = true;
return;
}
if (!impressions[id].length) {
return;
}
// If we don't want to store impressions older than the longest period
if (item.frequency.custom && !item.frequency.lifetime) {
const now = Date.now();
impressions[id] = impressions[id].filter(t => (now - t) < this.getLongestPeriod(item));
needsUpdate = true;
}
});
if (needsUpdate) {
this._storage.set(impressionsString, impressions);
}
return impressions;
}
async sendNextMessage(target, trigger) {
const msgs = this._getUnblockedMessages();
let message = null;
@ -490,7 +602,7 @@ class _ASRouter {
if (previewMsgs.length) {
[message] = previewMsgs;
} else {
message = await this._findMessage(msgs, target, trigger);
message = await this._findMessage(msgs, trigger);
}
if (previewMsgs.length) {
@ -512,30 +624,30 @@ class _ASRouter {
await this._sendMessageToTarget(newMessage, target, force, action.data);
}
async blockById(idOrIds) {
async blockMessageById(idOrIds) {
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
await this.setState(state => {
const blockList = [...state.blockList, ...idsToBlock];
const messageBlockList = [...state.messageBlockList, ...idsToBlock];
// When a message is blocked, its impressions should be cleared as well
const impressions = {...state.impressions};
idsToBlock.forEach(id => delete impressions[id]);
this._storage.set("blockList", blockList);
return {blockList, impressions};
const messageImpressions = {...state.messageImpressions};
idsToBlock.forEach(id => delete messageImpressions[id]);
this._storage.set("messageBlockList", messageBlockList);
return {messageBlockList, messageImpressions};
});
}
openLinkIn(url, target, {isPrivate = false, trusted = false, where = ""}) {
const win = target.browser.ownerGlobal;
const params = {
private: isPrivate,
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})
};
if (trusted) {
win.openTrustedLinkIn(url, where);
} else {
win.openLinkIn(url, where, params);
}
async blockProviderById(idOrIds) {
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
await this.setState(state => {
const providerBlockList = [...state.providerBlockList, ...idsToBlock];
// When a provider is blocked, its impressions should be cleared as well
const providerImpressions = {...state.providerImpressions};
idsToBlock.forEach(id => delete providerImpressions[id]);
this._storage.set("providerBlockList", providerBlockList);
return {providerBlockList, providerImpressions};
});
}
_validPreviewEndpoint(url) {
@ -579,7 +691,7 @@ class _ASRouter {
// To be passed to ASRouterTriggerListeners
async _triggerHandler(target, trigger) {
await this.onMessage({target, data: {type: "TRIGGER", trigger}});
await this.onMessage({target, data: {type: "TRIGGER", data: {trigger}}});
}
_removePreviewEndpoint(state) {
@ -602,19 +714,13 @@ class _ASRouter {
target.browser.ownerGlobal.OpenBrowserWindow({private: true});
break;
case ra.OPEN_URL:
this.openLinkIn(action.data.url, target, {
isPrivate: false,
where: "tabshifted",
triggeringPrincipal: Services.scriptSecurityManager.getNullPrincipal({})
target.browser.ownerGlobal.openLinkIn(action.data.url, "tabshifted", {
private: false,
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})
});
break;
case ra.OPEN_ABOUT_PAGE:
this.openLinkIn(`about:${action.data.page}`, target, {
isPrivate: false,
trusted: true,
where: "tab",
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
target.browser.ownerGlobal.openTrustedLinkIn(`about:${action.data.page}`, "tab");
break;
case ra.OPEN_APPLICATIONS_MENU:
UITour.showMenu(target.browser.ownerGlobal, action.data.target);
@ -625,6 +731,10 @@ class _ASRouter {
}
}
dispatch(action, target) {
this.onMessage({data: action, target});
}
async onMessage({data: action, target}) {
switch (action.type) {
case "USER_ACTION":
@ -645,29 +755,41 @@ class _ASRouter {
await this.sendNextMessage(target, (action.data && action.data.trigger) || {});
break;
case "BLOCK_MESSAGE_BY_ID":
await this.blockById(action.data.id);
await this.blockMessageById(action.data.id);
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: action.data.id}});
break;
case "BLOCK_PROVIDER_BY_ID":
await this.blockProviderById(action.data.id);
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: action.data.id}});
break;
case "BLOCK_BUNDLE":
await this.blockById(action.data.bundle.map(b => b.id));
await this.blockMessageById(action.data.bundle.map(b => b.id));
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
break;
case "UNBLOCK_MESSAGE_BY_ID":
await this.setState(state => {
const blockList = [...state.blockList];
blockList.splice(blockList.indexOf(action.data.id), 1);
this._storage.set("blockList", blockList);
return {blockList};
const messageBlockList = [...state.messageBlockList];
messageBlockList.splice(messageBlockList.indexOf(action.data.id), 1);
this._storage.set("messageBlockList", messageBlockList);
return {messageBlockList};
});
break;
case "UNBLOCK_PROVIDER_BY_ID":
await this.setState(state => {
const providerBlockList = [...state.providerBlockList];
providerBlockList.splice(providerBlockList.indexOf(action.data.id), 1);
this._storage.set("providerBlockList", providerBlockList);
return {providerBlockList};
});
break;
case "UNBLOCK_BUNDLE":
await this.setState(state => {
const blockList = [...state.blockList];
const messageBlockList = [...state.messageBlockList];
for (let message of action.data.bundle) {
blockList.splice(blockList.indexOf(message.id), 1);
messageBlockList.splice(messageBlockList.indexOf(message.id), 1);
}
this._storage.set("blockList", blockList);
return {blockList};
this._storage.set("messageBlockList", messageBlockList);
return {messageBlockList};
});
break;
case "OVERRIDE_MESSAGE":
@ -682,7 +804,7 @@ class _ASRouter {
}
break;
case "IMPRESSION":
this.addImpression(action.data);
await this.addImpression(action.data);
break;
}
}

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

@ -39,10 +39,9 @@ class ASRouterFeed {
enableOrDisableBasedOnPref() {
const prefs = this.store.getState().Prefs.values;
const isExperimentEnabled = prefs.asrouterExperimentEnabled;
const isOnboardingExperimentEnabled = prefs.asrouterOnboardingCohort;
if (!this.router.initialized && (isExperimentEnabled || isOnboardingExperimentEnabled > 0)) {
if (!this.router.initialized && isExperimentEnabled) {
this.enable();
} else if ((!isExperimentEnabled || isOnboardingExperimentEnabled === 0) && this.router.initialized) {
} else if (!isExperimentEnabled && this.router.initialized) {
this.disable();
}
}
@ -53,7 +52,7 @@ class ASRouterFeed {
this.enableOrDisableBasedOnPref();
break;
case at.PREF_CHANGED:
if (["asrouterOnboardingCohort", "asrouterExperimentEnabled"].includes(action.data.name)) {
if (action.data.name === "asrouterExperimentEnabled") {
this.enableOrDisableBasedOnPref();
}
break;

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

@ -13,13 +13,9 @@ ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
"resource://gre/modules/TelemetryEnvironment.jsm");
const FXA_USERNAME_PREF = "services.sync.username";
const ONBOARDING_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouterOnboardingCohort";
const ONBOARDING_MESSAGE_PROVDIER_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const MOZ_JEXL_FILEPATH = "mozjexl";
// Max possible cap for any message
const MAX_LIFETIME_CAP = 100;
const ONE_DAY = 24 * 60 * 60 * 1000;
const {activityStreamProvider: asProvider} = NewTabUtils;
const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
@ -71,6 +67,9 @@ const TargetingGetters = {
update: settings.update
};
},
get currentDate() {
return new Date();
},
get profileAgeCreated() {
return new ProfileAge(null, null).created;
},
@ -109,7 +108,6 @@ const TargetingGetters = {
return {addons: info, isFullData: fullData};
});
},
get searchEngines() {
return new Promise(resolve => {
// Note: calling init ensures this code is only executed after Search has been initialized
@ -128,18 +126,15 @@ const TargetingGetters = {
});
});
},
get isDefaultBrowser() {
try {
return ShellService.isDefaultBrowser();
} catch (e) {}
return null;
},
get devToolsOpenedCount() {
return Services.prefs.getIntPref("devtools.selfxss.count");
},
get topFrecentSites() {
return TopFrecentSitesCache.topFrecentSites.then(sites => sites.map(site => (
{
@ -150,10 +145,16 @@ const TargetingGetters = {
}
)));
},
// Temporary targeting function for the purposes of running the simplified onboarding experience
get isInExperimentCohort() {
return Services.prefs.getIntPref(ONBOARDING_EXPERIMENT_PREF, 0);
const allProviders = Services.prefs.getStringPref(ONBOARDING_MESSAGE_PROVDIER_EXPERIMENT_PREF, "");
try {
const {cohort} = JSON.parse(allProviders).find(i => i.id === "onboarding");
return (typeof cohort === "number" ? cohort : 0);
} catch (e) {
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
}
return 0;
}
};
@ -178,35 +179,6 @@ this.ASRouterTargeting = {
return candidateMessageTrigger.params.includes(trigger.param);
},
isBelowFrequencyCap(message, impressionsForMessage) {
if (!message.frequency || !impressionsForMessage || !impressionsForMessage.length) {
return true;
}
if (
message.frequency.lifetime &&
impressionsForMessage.length >= Math.min(message.frequency.lifetime, MAX_LIFETIME_CAP)
) {
return false;
}
if (message.frequency.custom) {
const now = Date.now();
for (const setting of message.frequency.custom) {
let {period} = setting;
if (period === "daily") {
period = ONE_DAY;
}
const impressionsInPeriod = impressionsForMessage.filter(t => (now - t) < period);
if (impressionsInPeriod.length >= setting.cap) {
return false;
}
}
}
return true;
},
/**
* checkMessageTargeting - Checks is a message's targeting parameters are satisfied
*
@ -244,7 +216,7 @@ this.ASRouterTargeting = {
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
* @returns {obj} an AS router message
*/
async findMatchingMessage({messages, impressions = {}, trigger, context, onError}) {
async findMatchingMessage({messages, trigger, context, onError}) {
const arrayOfItems = [...messages];
let match;
let candidate;
@ -255,7 +227,6 @@ this.ASRouterTargeting = {
if (
candidate &&
(trigger ? this.isTriggerMatch(trigger, candidate.trigger) : !candidate.trigger) &&
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
// If a trigger expression was passed to this function, the message should match it.
// Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time)
await this.checkMessageTargeting(candidate, context, onError)

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

@ -77,7 +77,7 @@ this.ASRouterTriggerListeners = new Map([
try {
const host = (new URL(location)).hostname;
if (this._hosts.has(host)) {
this._triggerHandler(aBrowser.messageManager, {id: "openURL", param: host});
this._triggerHandler(aBrowser, {id: "openURL", param: host});
}
} catch (e) {} // Couldn't parse location URL
}

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

@ -192,10 +192,6 @@ const PREFS_CONFIG = new Map([
title: "Is the message center experiment on?",
value: false
}],
["asrouterOnboardingCohort", {
title: "What cohort is the user in?",
value: 0
}],
["asrouter.messageProviders", {
title: "Configuration for ASRouter message providers",
@ -203,16 +199,26 @@ const PREFS_CONFIG = new Map([
* Each provider must have a unique id and a type of "local" or "remote".
* Local providers must specify the name of an ASRouter message provider.
* Remote providers must specify a `url` and an `updateCycleInMs`.
* Each provider must also have an `enabled` boolean.
*/
value: JSON.stringify([{
id: "onboarding",
type: "local",
localProvider: "OnboardingMessageProvider"
localProvider: "OnboardingMessageProvider",
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release",
cohort: 0
}, {
id: "snippets",
type: "remote",
url: "https://activity-stream-icons.services.mozilla.com/v1/messages.json.br",
updateCycleInMs: ONE_HOUR_IN_MS * 4
url: "https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json",
updateCycleInMs: ONE_HOUR_IN_MS * 4,
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release"
}, {
id: "cfr",
type: "local",
localProvider: "CFRMessageProvider",
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release",
cohort: 0
}])
}]
]);

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

@ -0,0 +1,286 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const BASE_ADDONS_DOWNLOAD_URL = "https://addons.mozilla.org/firefox/downloads/file";
const AMAZON_ASSISTANT_PARAMS = {
existing_addons: ["abb@amazon.com", "{75c7fe97-5a90-4b54-9052-3534235eaf41}", "{ef34596e-1e43-4e84-b2ff-1e58e287e08d}", "{ea280feb-155a-492e-8016-ac96dd995f2c}", "izer@camelcamelcamel.com", "amptra@keepa.com", "pricealarm@icopron.ch", "{774f76c7-6807-481e-bf64-f9b7d5cda602}"],
open_urls: ["smile.amazon.com", "www.audible.com", "www.amazon.com", "amazon.com", "audible.com"],
sumo_path: "extensionpromotions"
};
const FACEBOOK_CONTAINER_PARAMS = {
existing_addons: ["@contain-facebook", "{bb1b80be-e6b3-40a1-9b6e-9d4073343f0b}", "{a50d61ca-d27b-437a-8b52-5fd801a0a88b}"],
open_urls: ["www.facebook.com", "facebook.com"],
sumo_path: "extensionrecommendations"
};
const GOOGLE_TRANSLATE_PARAMS = {
existing_addons: ["jid1-93WyvpgvxzGATw@jetpack", "{087ef4e1-4286-4be6-9aa3-8d6c420ee1db}", "{4170faaa-ee87-4a0e-b57a-1aec49282887}", "jid1-TMndP6cdKgxLcQ@jetpack",
"s3google@translator", "{9c63d15c-b4d9-43bd-b223-37f0a1f22e2a}", "translator@zoli.bod", "{8cda9ce6-7893-4f47-ac70-a65215cec288}", "simple-translate@sienori", "@translatenow",
"{a79fafce-8da6-4685-923f-7ba1015b8748})", "{8a802b5a-eeab-11e2-a41d-b0096288709b}", "jid0-fbHwsGfb6kJyq2hj65KnbGte3yT@jetpack", "storetranslate.plugin@gmail.com",
"jid1-r2tWDbSkq8AZK1@jetpack", "{b384b75c-c978-4c4d-b3cf-62a82d8f8f12}", "jid1-f7dnBeTj8ElpWQ@jetpack", "{dac8a935-4775-4918-9205-5c0600087dc4}", "gtranslation2@slam.com",
"{e20e0de5-1667-4df4-bd69-705720e37391}", "{09e26ae9-e9c1-477c-80a6-99934212f2fe}", "mgxtranslator@magemagix.com", "gtranslatewins@mozilla.org"],
open_urls: ["translate.google.com"],
sumo_path: "extensionrecommendations"
};
const YOUTUBE_ENHANCE_PARAMS = {
existing_addons: ["enhancerforyoutube@maximerf.addons.mozilla.org", "{dc8f61ab-5e98-4027-98ef-bb2ff6060d71}", "{7b1bf0b6-a1b9-42b0-b75d-252036438bdc}", "jid0-UVAeBCfd34Kk5usS8A1CBiobvM8@jetpack",
"iridium@particlecore.github.io", "jid1-ss6kLNCbNz6u0g@jetpack", "{1cf918d2-f4ea-4b4f-b34e-455283fef19f}"],
open_urls: ["www.youtube.com", "youtube.com"],
sumo_path: "extensionrecommendations"
};
const WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS = {
existing_addons: ["@wikipediacontextmenusearch", "{ebf47fc8-01d8-4dba-aa04-2118402f4b20}", "{5737a280-b359-4e26-95b0-adec5915a854}", "olivier.debroqueville@gmail.com", "{3923146e-98cb-472b-9c13-f6849d34d6b8}"],
open_urls: ["www.wikipedia.org", "wikipedia.org"],
sumo_path: "extensionrecommendations"
};
const REDDIT_ENHANCEMENT_PARAMS = {
existing_addons: ["jid1-xUfzOsOFlzSOXg@jetpack"],
open_urls: ["www.reddit.com", "reddit.com"],
sumo_path: "extensionrecommendations"
};
const CFR_MESSAGES = [
{
id: "AMAZON_ASSISTANT_1",
template: "cfr_doorhanger",
content: {
notification_text: "Recommendation",
heading_text: "Recommended Extension",
info_icon: {
label: "why_seeing_this",
sumo_path: AMAZON_ASSISTANT_PARAMS.sumo_path
},
addon: {
title: "Amazon Assistant",
icon: "resource://activity-stream/data/content/assets/cfr_amazon_assistant.png",
author: "Amazon",
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/amazon-browser-bar/"
},
text: "Amazon Assistant helps you make better shopping decisions by showing product comparisons at thousands of retail sites.",
buttons: {
primary: {
label: "Add to Firefox",
accessKey: "A",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/950930/amazon_assistant_for_firefox-10.1805.2.1019-an+fx.xpi`}
}
},
secondary: {
label: "No Thanks",
accessKey: "N",
action: {type: "CANCEL"}
}
}
},
frequency: {lifetime: 1},
targeting: `
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
trigger: {id: "openURL", params: AMAZON_ASSISTANT_PARAMS.open_urls}
},
{
id: "FACEBOOK_CONTAINER_1",
template: "cfr_doorhanger",
content: {
notification_text: "Recommendation",
heading_text: "Recommended Extension",
info_icon: {
label: "why_seeing_this",
sumo_path: FACEBOOK_CONTAINER_PARAMS.sumo_path
},
addon: {
title: "Facebook Container",
icon: "resource://activity-stream/data/content/assets/cfr_fb_container.png",
author: "Mozilla",
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/facebook-container/"
},
text: "Stop Facebook from tracking your activity across the web. Use Facebook the way you normally do without annoying ads following you around.",
buttons: {
primary: {
label: "Add to Firefox",
accessKey: "A",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/918624/facebook_container-1.3.1-an+fx-linux.xpi`}
}
},
secondary: {
label: "No Thanks",
accessKey: "N",
action: {type: "CANCEL"}
}
}
},
frequency: {lifetime: 1},
targeting: `
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
trigger: {id: "openURL", params: FACEBOOK_CONTAINER_PARAMS.open_urls}
},
{
id: "GOOGLE_TRANSLATE_1",
template: "cfr_doorhanger",
content: {
notification_text: "Recommendation",
heading_text: "Recommended Extension",
info_icon: {
label: "why_seeing_this",
sumo_path: GOOGLE_TRANSLATE_PARAMS.sumo_path
},
addon: {
title: "To Google Translate",
icon: "resource://activity-stream/data/content/assets/cfr_google_translate.png",
author: "Juan Escobar",
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/to-google-translate/"
},
text: "Instantly translate any webpage text. Simply highlight the text, right-click to open the context menu, and choose a text or aural translation.",
buttons: {
primary: {
label: "Add to Firefox",
accessKey: "A",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/1008798/al_traductor_de_google-3.3-an+fx.xpi`}
}
},
secondary: {
label: "No Thanks",
accessKey: "N",
action: {type: "CANCEL"}
}
}
},
frequency: {lifetime: 1},
targeting: `
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
trigger: {id: "openURL", params: GOOGLE_TRANSLATE_PARAMS.open_urls}
},
{
id: "YOUTUBE_ENHANCE_1",
template: "cfr_doorhanger",
content: {
notification_text: "Recommendation",
heading_text: "Recommended Extension",
info_icon: {
label: "why_seeing_this",
sumo_path: YOUTUBE_ENHANCE_PARAMS.sumo_path
},
addon: {
title: "Enhancer for YouTube\u2122",
icon: "resource://activity-stream/data/content/assets/cfr_enhancer_youtube.png",
author: "Maxime RF",
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/enhancer-for-youtube/"
},
text: "Take control of your YouTube experience. Automatically block annoying ads, set playback speed and volume, remove annotations, and more.",
buttons: {
primary: {
label: "Add to Firefox",
accessKey: "A",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/1028400/enhancer_for_youtubetm-2.0.73-an+fx-linux.xpi`}
}
},
secondary: {
label: "No Thanks",
accessKey: "N",
action: {type: "CANCEL"}
}
}
},
frequency: {lifetime: 1},
targeting: `
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
trigger: {id: "openURL", params: YOUTUBE_ENHANCE_PARAMS.open_urls}
},
{
id: "WIKIPEDIA_CONTEXT_MENU_SEARCH_1",
template: "cfr_doorhanger",
content: {
notification_text: "Recommendation",
heading_text: "Recommended Extension",
info_icon: {
label: "why_seeing_this",
sumo_path: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.sumo_path
},
addon: {
title: "Wikipedia Context Menu Search",
icon: "resource://activity-stream/data/content/assets/cfr_wiki_search.png",
author: "Nick Diedrich",
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/wikipedia-context-menu-search/"
},
text: "Get to a Wikipedia page fast, from anywhere on the web. Just highlight any webpage text and right-click to open the context menu to start a Wikipedia search.",
buttons: {
primary: {
label: "Add to Firefox",
accessKey: "A",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/890224/wikipedia_context_menu_search-1.8-an+fx.xpi`}
}
},
secondary: {
label: "No Thanks",
accessKey: "N",
action: {type: "CANCEL"}
}
}
},
frequency: {lifetime: 1},
targeting: `
(${JSON.stringify(WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
trigger: {id: "openURL", params: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls}
},
{
id: "REDDIT_ENHANCEMENT_1",
template: "cfr_doorhanger",
content: {
notification_text: "Recommendation",
heading_text: "Recommended Extension",
info_icon: {
label: "why_seeing_this",
sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path
},
addon: {
title: "Reddit Enhancement Suite",
icon: "resource://activity-stream/data/content/assets/cfr_reddit_enhancement.png",
author: "honestbleeps",
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/reddit-enhancement-suite/"
},
text: "New features include Inline Image Viewer, Never Ending Reddit (never click 'next page' again), Keyboard Navigation, Account Switcher, and User Tagger.",
buttons: {
primary: {
label: "Add to Firefox",
accessKey: "A",
action: {
type: "INSTALL_ADDON_FROM_URL",
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/991623/reddit_enhancement_suite-5.12.5-an+fx.xpi`}
}
},
secondary: {
label: "No Thanks",
accessKey: "N",
action: {type: "CANCEL"}
}
}
},
frequency: {lifetime: 1},
targeting: `
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
trigger: {id: "openURL", params: REDDIT_ENHANCEMENT_PARAMS.open_urls}
}
];
const CFRMessageProvider = {
getMessages() {
return CFR_MESSAGES;
}
};
this.CFRMessageProvider = CFRMessageProvider;
const EXPORTED_SYMBOLS = ["CFRMessageProvider"];

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

@ -34,10 +34,14 @@ class PageAction {
this.button = win.document.getElementById("cfr-button");
this.label = win.document.getElementById("cfr-label");
// This should NOT be use directly to dispatch message-defined actions attached to buttons.
// Please use dispatchUserAction instead.
this._dispatchToASRouter = dispatchToASRouter;
this._popupStateChange = this._popupStateChange.bind(this);
this._collapse = this._collapse.bind(this);
this._handleClick = this._handleClick.bind(this);
this.dispatchUserAction = this.dispatchUserAction.bind(this);
// Saved timeout IDs for scheduled state changes, so they can be cancelled
this.stateTransitionTimeoutIDs = [];
@ -74,6 +78,13 @@ class PageAction {
this.urlbar.removeAttribute("cfr-recommendation-state");
}
dispatchUserAction(action) {
this._dispatchToASRouter(
{type: "USER_ACTION", data: action},
{browser: this.window.gBrowser.selectedBrowser}
);
}
_expand(delay = 0) {
if (!delay) {
// Non-delayed state change overrides any scheduled state changes
@ -144,7 +155,7 @@ class PageAction {
const mainAction = {
label: primary.label,
accessKey: primary.accessKey,
callback: () => this._dispatchToASRouter(primary.action)
callback: () => this.dispatchUserAction(primary.action)
};
const secondaryActions = [{
@ -243,5 +254,6 @@ const CFRPageActions = {
RecommendationMap.clear();
}
};
this.CFRPageActions = CFRPageActions;
const EXPORTED_SYMBOLS = ["CFRPageActions"];

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

@ -364,6 +364,12 @@ this.TelemetryFeed = class TelemetryFeed {
);
}
/**
* Create a ping for AS router event. The client_id is set to "n/a" by default,
* AS router components could change that by including a boolean "includeClientID"
* to the payload of the action, impression_id would be set to "n/a" at the same time.
* Note that "includeClientID" will not be included in the result ping.
*/
createASRouterEvent(action) {
const ping = {
client_id: "n/a",
@ -371,6 +377,12 @@ this.TelemetryFeed = class TelemetryFeed {
locale: Services.locale.getAppLocaleAsLangTag(),
impression_id: this._impressionId
};
if (action.data.includeClientID) {
// Ping-centre client will fill in the client_id if it's not provided in the ping
delete ping.client_id;
delete action.data.includeClientID;
ping.impression_id = "n/a";
}
return Object.assign(ping, action.data);
}

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

@ -311,17 +311,25 @@ this.TopStoriesFeed = class TopStoriesFeed {
return this.show_spocs && this.store.getState().Prefs.values.showSponsored;
}
dispatchSpocDone(target) {
const action = {type: at.POCKET_WAITING_FOR_SPOC, data: false};
this.store.dispatch(ac.OnlyToOneContent(action, target));
}
maybeAddSpoc(target) {
const updateContent = () => {
if (!this.shouldShowSpocs()) {
this.dispatchSpocDone(target);
return false;
}
if (Math.random() > this.spocsPerNewTabs) {
this.dispatchSpocDone(target);
return false;
}
if (!this.spocs || !this.spocs.length) {
// We have stories but no spocs so there's nothing to do and this update can be
// removed from the queue.
this.dispatchSpocDone(target);
return false;
}
@ -331,6 +339,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
if (!spocs.length) {
// There's currently no spoc left to display
this.dispatchSpocDone(target);
return false;
}
@ -342,6 +351,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
// Send a content update to the target tab
const action = {type: at.SECTION_UPDATE, data: Object.assign({rows}, {id: SECTION_ID})};
this.store.dispatch(ac.OnlyToOneContent(action, target));
this.dispatchSpocDone(target);
return false;
};

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=وسّع القسم
section_menu_action_manage_section=أدِر القسم
section_menu_action_manage_webext=أدِر الامتداد
section_menu_action_add_topsite=أضف موقعًا شائعًا
section_menu_action_add_search_engine=أضِف محرك بحث
section_menu_action_move_up=انقل لأعلى
section_menu_action_move_down=انقل لأسفل
section_menu_action_privacy_notice=تنويه الخصوصية
@ -201,4 +202,3 @@ firstrun_privacy_notice=تنويه الخصوصية
firstrun_continue_to_login=تابِع
firstrun_skip_login=تجاوز هذه الخطوة
section_menu_action_add_search_engine=أضِف محرك بحث

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Разгъване на раздела
section_menu_action_manage_section=Управление на раздела
section_menu_action_manage_webext=Управление на добавката
section_menu_action_add_topsite=Добавяне на често посещавана страница
section_menu_action_add_search_engine=Добавяне на търсеща машина
section_menu_action_move_up=Преместване нагоре
section_menu_action_move_down=Преместване надолу
section_menu_action_privacy_notice=Политика за личните данни
@ -201,4 +202,3 @@ firstrun_privacy_notice=Политиката за лични данни
firstrun_continue_to_login=Продължаване
firstrun_skip_login=Пропускане
section_menu_action_add_search_engine=Добавяне на търсеща машина

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=সেকশনটি প্রসারি
section_menu_action_manage_section=সেকশনটি পরিচালনা করুন
section_menu_action_manage_webext=এক্সটেনসন ব্যবহার করুন
section_menu_action_add_topsite=টপ সাইট যোগ করুন
section_menu_action_add_search_engine=অনুসন্ধান ইঞ্জিন যোগ করুন
section_menu_action_move_up=উপরে উঠাও
section_menu_action_move_down=নীচে নামাও
section_menu_action_privacy_notice=গোপনীয়তা নীতি
@ -180,19 +181,24 @@ section_menu_action_privacy_notice=গোপনীয়তা নীতি
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=অাপনি Firefox ব্যবহার করুন
firstrun_content=আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।
firstrun_learn_more_link=Firefox অ্যাকাউন্ট সম্পর্কে আরও জানুন
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=আপনার ই-মেইল লিখুন
firstrun_form_sub_header=Firefox সিঙ্ক চালিয়ে যেতে
firstrun_email_input_placeholder=ইমেইল
firstrun_invalid_input=কার্যকর ইমেইল আবশ্যক
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।
firstrun_terms_of_service=সেবার শর্ত
firstrun_privacy_notice=গোপনীয়তা নীতি
firstrun_continue_to_login=চালিয়ে যান
firstrun_skip_login=এই ধাপটি বাদ দিন
section_menu_action_add_search_engine=অনুসন্ধান ইঞ্জিন যোগ

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

@ -60,7 +60,7 @@ menu_action_open_file=Obre el fitxer
# link that belongs to this downloaded item"
menu_action_copy_download_link=Copia l'enllaç de la baixada
menu_action_go_to_download_page=Vés a la pàgina de la baixada
menu_action_remove_download=Suprimeix de l'historial
menu_action_remove_download=Elimina de l'historial
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
@ -173,6 +173,7 @@ section_menu_action_expand_section=Amplia la secció
section_menu_action_manage_section=Gestiona la secció
section_menu_action_manage_webext=Gestiona l'extensió
section_menu_action_add_topsite=Afegeix com a lloc principal
section_menu_action_add_search_engine=Afegeix un motor de cerca
section_menu_action_move_up=Mou cap amunt
section_menu_action_move_down=Mou cap avall
section_menu_action_privacy_notice=Avís de privadesa
@ -201,4 +202,3 @@ firstrun_privacy_notice=Avís de privadesa
firstrun_continue_to_login=Continua
firstrun_skip_login=Omet aquest pas
section_menu_action_add_search_engine=Afegeix un motor de cerca

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Wótrězk pokazaś
section_menu_action_manage_section=Wótrězk zastojaś
section_menu_action_manage_webext=Rozšyrjenje zastojaś
section_menu_action_add_topsite=Woblubowane sedło pśidaś
section_menu_action_add_search_engine=Pytnicu pśidaś
section_menu_action_move_up=Górjej
section_menu_action_move_down=Dołoj
section_menu_action_privacy_notice=Powěźeńka priwatnosći
@ -201,4 +202,3 @@ firstrun_privacy_notice=Powěźeńka priwatnosći
firstrun_continue_to_login=Dalej
firstrun_skip_login=Toś ten kšac pśeskócyś
section_menu_action_add_search_engine=Pytnicu pśidaś

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Malfaldi sekcion
section_menu_action_manage_section=Administri sekcion
section_menu_action_manage_webext=Administri etendaĵon
section_menu_action_add_topsite=Aldoni oftan retejon
section_menu_action_add_search_engine=Aldoni serĉilon
section_menu_action_move_up=Movi supren
section_menu_action_move_down=Movi malsupren
section_menu_action_privacy_notice=Rimarko pri privateco
@ -201,4 +202,3 @@ firstrun_privacy_notice=rimarkon pri privateco
firstrun_continue_to_login=Daŭrigi
firstrun_skip_login=Pretersalti tiun ĉi paŝon
section_menu_action_add_search_engine=Aldoni serĉilon

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Expandir sección
section_menu_action_manage_section=Gestionar sección
section_menu_action_manage_webext=Gestionar extensión
section_menu_action_add_topsite=Añadir sitio popular
section_menu_action_add_search_engine=Añadir motor de búsqueda
section_menu_action_move_up=Subir
section_menu_action_move_down=Bajar
section_menu_action_privacy_notice=Aviso de privacidad
@ -201,4 +202,3 @@ firstrun_privacy_notice=Aviso de privacidad
firstrun_continue_to_login=Continuar
firstrun_skip_login=Saltar este paso
section_menu_action_add_search_engine=Añadir motor de búsqueda

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Ampliar la sección
section_menu_action_manage_section=Administrar sección
section_menu_action_manage_webext=Gestionar extensión
section_menu_action_add_topsite=Agregar sitio popular
section_menu_action_add_search_engine=Agregar motor de búsqueda
section_menu_action_move_up=Subir
section_menu_action_move_down=Bajar
section_menu_action_privacy_notice=Política de privacidad
@ -201,4 +202,3 @@ firstrun_privacy_notice=Política de privacidad
firstrun_continue_to_login=Continuar
firstrun_skip_login=Saltar este paso
section_menu_action_add_search_engine=Agregar motor de búsqueda

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Zabaldu atala
section_menu_action_manage_section=Kudeatu atala
section_menu_action_manage_webext=Kudeatu hedapena
section_menu_action_add_topsite=Gehitu maiz erabilitako gunea
section_menu_action_add_search_engine=Gehitu bilaketa-motorra
section_menu_action_move_up=Eraman gora
section_menu_action_move_down=Eraman behera
section_menu_action_privacy_notice=Pribatutasun-oharra
@ -191,6 +192,8 @@ firstrun_form_sub_header=Firefox Sync-ekin jarraitzeko.
firstrun_email_input_placeholder=Helbide elektronikoa
firstrun_invalid_input=Baliozko helbide elektronikoa behar da
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Jarraitzearekin bat, {terms} eta {privacy} onartzen dituzu.
@ -199,4 +202,3 @@ firstrun_privacy_notice=Pribatutasun-oharra
firstrun_continue_to_login=Jarraitu
firstrun_skip_login=Saltatu urrats hau
section_menu_action_add_search_engine=Gehitu bilaketa-motorra

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=વિભાગ વિસ્તૃત કર
section_menu_action_manage_section=વિભાગ સંચાલિત કરો
section_menu_action_manage_webext=એક્સ્ટેંશનનો વહીવટ કરો
section_menu_action_add_topsite=ટોચની સાઇટ ઉમેરો
section_menu_action_add_search_engine=શોધ એંજીન ઉમેરો
section_menu_action_move_up=ઉપર કરો
section_menu_action_move_down=નીચે કરો
section_menu_action_privacy_notice=ખાનગી સૂચના
@ -201,4 +202,3 @@ firstrun_privacy_notice=ખાનગી સૂચના
firstrun_continue_to_login=ચાલુ રાખો
firstrun_skip_login=આ પગલું છોડી દો
section_menu_action_add_search_engine=શોધ યંત્ર ઉમેરો

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=अनुभाग विस्तृत क
section_menu_action_manage_section=अनुभाग प्रबंधित करें
section_menu_action_manage_webext=विस्तारक प्रबंधित करें
section_menu_action_add_topsite=शीर्ष साइट जोड़ें
section_menu_action_add_search_engine=खोज ईंजन जोड़ें
section_menu_action_move_up=ऊपर जाएँ
section_menu_action_move_down=नीचे जाएँ
section_menu_action_privacy_notice=गोपनीयता नीति
@ -201,4 +202,3 @@ firstrun_privacy_notice=गोपनीयता नीति
firstrun_continue_to_login=जारी रखें
firstrun_skip_login=इस चरण को छोड़ दें
section_menu_action_add_search_engine=सर्च इंजन जोड़े

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Wotrězk pokazać
section_menu_action_manage_section=Wotrězk rjadować
section_menu_action_manage_webext=Rozšěrjenje rjadować
section_menu_action_add_topsite=Woblubowane sydło přidać
section_menu_action_add_search_engine=Pytawu přidać
section_menu_action_move_up=Horje
section_menu_action_move_down=Dele
section_menu_action_privacy_notice=Zdźělenka priwatnosće
@ -201,4 +202,3 @@ firstrun_privacy_notice=Zdźělenka priwatnosće
firstrun_continue_to_login=Pokročować
firstrun_skip_login=Tutón krok přeskočić
section_menu_action_add_search_engine=Pytawu přidać

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Expander le section
section_menu_action_manage_section=Gerer le section
section_menu_action_manage_webext=Gerer extension
section_menu_action_add_topsite=Adder a sito popular
section_menu_action_add_search_engine=Adder un motor de recerca
section_menu_action_move_up=Mover in alto
section_menu_action_move_down=Mover in basso
section_menu_action_privacy_notice=Notification de confidentialitate
@ -201,4 +202,3 @@ firstrun_privacy_notice=Notification de confidentialitate
firstrun_continue_to_login=Continuar
firstrun_skip_login=Saltar iste grado
section_menu_action_add_search_engine=Adder un motor de recerca

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

@ -125,9 +125,9 @@ topsites_form_add_header=ახალი საიტი რჩეულებ
topsites_form_edit_header=რჩეული საიტის ჩასწორება
topsites_form_title_label=დასახელება
topsites_form_title_placeholder=სათაურის შეყვანა
topsites_form_url_label=URL ბმული
topsites_form_image_url_label=სასურველი სურათის URL ბმული
topsites_form_url_placeholder=აკრიფეთ ან ჩასვით URL ბმული
topsites_form_url_label=URL-ბმული
topsites_form_image_url_label=სასურველი სურათის URL-ბმული
topsites_form_url_placeholder=აკრიფეთ ან ჩასვით URL-ბმული
topsites_form_use_image_link=სასურველი სურათის გამოყენება…
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
topsites_form_preview_button=შეთვალიერება
@ -135,7 +135,7 @@ topsites_form_add_button=დამატება
topsites_form_save_button=შენახვა
topsites_form_cancel_button=გაუქმება
topsites_form_url_validation=საჭიროა მართებული URL
topsites_form_image_validation=სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL ბმული.
topsites_form_image_validation=სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL-ბმული.
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
# trending stories section and precedes a list of links to popular topics.
@ -182,7 +182,7 @@ section_menu_action_privacy_notice=პირადი მონაცემე
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=თან წაიყოლეთ Firefox
firstrun_content=მიიღეთ წვდომა თქვენს სანიშნებთან, ისტორიასთან, პაროლებსა და სხვა პარამეტრებთან, ყველა თქვენს მოწყობილობაზე.
firstrun_learn_more_link=იხილეთ ვრცლად, Firefox ანგარიშების შესახებ
firstrun_learn_more_link=იხილეთ ვრცლად, Firefox-ანგარიშების შესახებ
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Snefli tigezmi
section_menu_action_manage_section=Sefrek tigezmi
section_menu_action_manage_webext=Sefrek asiɣzef
section_menu_action_add_topsite=Rnu asmel ifazen
section_menu_action_add_search_engine=Rnu amsedday n unadi
section_menu_action_move_up=Ali
section_menu_action_move_down=Ader
section_menu_action_privacy_notice=Tasertit n tbaḍnit
@ -201,4 +202,3 @@ firstrun_privacy_notice=Tasertit n tbaḍnit
firstrun_continue_to_login=Kemmel
firstrun_skip_login=Zgel amecwaṛ-agi
section_menu_action_add_search_engine=Rnu amsedday n unadi

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=섹션 열기
section_menu_action_manage_section=섹션 관리
section_menu_action_manage_webext=부가 기능 관리
section_menu_action_add_topsite=인기 사이트 추가
section_menu_action_add_search_engine=검색 엔진 추가
section_menu_action_move_up=위로 이동
section_menu_action_move_down=아래로 이동
section_menu_action_privacy_notice=개인 정보 보호 정책
@ -201,4 +202,3 @@ firstrun_privacy_notice=개인 정보 보호 정책
firstrun_continue_to_login=계속
firstrun_skip_login=단계 건너뛰기
section_menu_action_add_search_engine=검색 엔진 추가

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Išplėsti skiltį
section_menu_action_manage_section=Tvarkyti skiltį
section_menu_action_manage_webext=Tvarkyti priedą
section_menu_action_add_topsite=Pridėti lankomą svetainę
section_menu_action_add_search_engine=Pridėti ieškyklę
section_menu_action_move_up=Pakelti
section_menu_action_move_down=Nuleisti
section_menu_action_privacy_notice=Privatumo nuostatai
@ -201,4 +202,3 @@ firstrun_privacy_notice=privatumo nuostatais
firstrun_continue_to_login=Tęsti
firstrun_skip_login=Praleisti šį žingsnį
section_menu_action_add_search_engine=Pridėti ieškyklę

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

@ -50,6 +50,9 @@ menu_action_archive_pocket=Pocket मध्ये संग्रहित क
# "this action" is that it will show where the downloaded file exists on the file system
# for each operating system.
menu_action_show_file_mac_os=Finder मध्ये दर्शवा
menu_action_show_file_windows=समाविष्ट करणारे फोल्डर उघडा
menu_action_show_file_linux=समाविष्ट करणारे फोल्डर उघडा
menu_action_show_file_default=फाईल दाखवा
menu_action_open_file=फाइल उघडा
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
@ -95,9 +98,12 @@ prefs_section_rows_option={num} ओळ;{num} ओळी
prefs_search_header=वेब शोध
prefs_topsites_description=आपण सर्वाधिक भेट देता त्या साइट
prefs_topstories_description2=आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री
prefs_topstories_options_sponsored_label=प्रायोजित कथा
prefs_topstories_sponsored_learn_more=अधिक जाणून घ्या
prefs_highlights_description=आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा
prefs_highlights_options_visited_label=भेट दिलेली पृष्ठे
prefs_highlights_options_download_label=अलीकडचे डाउनलोड
prefs_highlights_options_pocket_label=Pocket मध्ये जतन केलेले पृष्ठ
prefs_snippets_description=Mozilla आणि Firefox कडून अद्यतने
settings_pane_button_label=आपले नवीन टॅब पृष्ठ सानुकूलित करा
settings_pane_topsites_header=शीर्ष साइट्स
@ -120,13 +126,16 @@ topsites_form_edit_header=खास साईट संपादित करा
topsites_form_title_label=शिर्षक
topsites_form_title_placeholder=शिर्षक प्रविष्ट करा
topsites_form_url_label=URL
topsites_form_image_url_label=सानुकूल प्रतिमा URL
topsites_form_url_placeholder=URL चिकटवा किंवा टाईप करा
topsites_form_use_image_link=सानुकूल प्रतिमा वापरा…
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
topsites_form_preview_button=पूर्वावलोकन
topsites_form_add_button=समाविष्ट करा
topsites_form_save_button=जतन करा
topsites_form_cancel_button=रद्द करा
topsites_form_url_validation=वैध URL आवश्यक
topsites_form_image_validation=प्रतिमा लोड झाली नाही. वेगळी URL वापरून पहा.
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
# trending stories section and precedes a list of links to popular topics.
@ -153,12 +162,18 @@ manual_migration_import_button=आता आयात करा
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
# action link are shown in each section of UI that fails to render
error_fallback_default_info=अरेरे, हा मजकूर लोड करताना काहीतरी गोंधळ झाला.
error_fallback_default_refresh_suggestion=पुन्हा प्रयत्न करण्यासाठी पृष्ठ रिफ्रेश करा.
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_remove_section=विभाग काढा
section_menu_action_collapse_section=विभाग ढासळा
section_menu_action_expand_section=विभाग वाढवा
section_menu_action_manage_section=विभाग व्यवस्थापित करा
section_menu_action_manage_webext=एक्सटेन्शन व्यवस्थापित करा
section_menu_action_add_topsite=खास साईट्स जोडा
section_menu_action_add_search_engine=शोध इंजीन जोडा
section_menu_action_move_up=वर जा
section_menu_action_move_down=खाली जा
section_menu_action_privacy_notice=गोपनीयता सूचना
@ -166,21 +181,24 @@ section_menu_action_privacy_notice=गोपनीयता सूचना
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=Firefox सोबत न्या
firstrun_content=आपले बुकमार्क्स, इतिहास, पासवर्ड आणि इतर सेटिंग आपल्या सर्व उपकरणांवर मिळवा.
firstrun_learn_more_link=Firefox खात्यांविषयी अधिक जाणून घ्या
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=ईमेल प्रविष्ट करा
firstrun_form_sub_header=Firefox Sync वर सुरू ठेवण्यासाठी
firstrun_email_input_placeholder=ईमेल
firstrun_invalid_input=वैध ईमेल आवश्यक
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=पुढे जाताना आपण {terms} आणि {privacy} यांना संमती देता.
firstrun_terms_of_service=सेवा अटी
firstrun_privacy_notice=गोपनीयता सूचना
firstrun_continue_to_login=पुढे चला
firstrun_skip_login=ही पायरी वगळा
section_menu_action_add_search_engine=शोध इंजीन जोडा

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

@ -172,6 +172,7 @@ section_menu_action_expand_section=Desplegar la seccion
section_menu_action_manage_section=Gerir la seccion
section_menu_action_manage_webext=Gerir lextension
section_menu_action_add_topsite=Apondre als sites populars
section_menu_action_add_search_engine=Apondre un motor de recèrca
section_menu_action_move_up=Desplaçar cap amont
section_menu_action_move_down=Desplaçar cap aval
section_menu_action_privacy_notice=Politica de confidencialitat
@ -200,4 +201,3 @@ firstrun_privacy_notice=Avís de privacitat
firstrun_continue_to_login=Contunhar
firstrun_skip_login=Passar aquesta etapa
section_menu_action_add_search_engine=Apondre un motor de recèrca

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

@ -99,6 +99,7 @@ section_menu_action_expand_section=Rozwiń sekcję
section_menu_action_manage_section=Zarządzaj sekcją
section_menu_action_manage_webext=Zarządzaj rozszerzeniem
section_menu_action_add_topsite=Dodaj stronę do popularnych
section_menu_action_add_search_engine=Dodaj wyszukiwarkę
section_menu_action_move_up=Przesuń w górę
section_menu_action_move_down=Przesuń w dół
section_menu_action_privacy_notice=Uwagi dotyczące prywatności
@ -115,4 +116,3 @@ firstrun_terms_of_service=warunki korzystania z usługi
firstrun_privacy_notice=uwagi dotyczące prywatności
firstrun_continue_to_login=Kontynuuj
firstrun_skip_login=Pomiń
section_menu_action_add_search_engine=Dodaj wyszukiwarkę

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=Развернуть раздел
section_menu_action_manage_section=Управление разделом
section_menu_action_manage_webext=Управление расширением
section_menu_action_add_topsite=Добавить в топ сайтов
section_menu_action_add_search_engine=Добавить поисковую систему
section_menu_action_move_up=Вверх
section_menu_action_move_down=Вниз
section_menu_action_privacy_notice=Уведомление о приватности
@ -201,4 +202,3 @@ firstrun_privacy_notice=политикой приватности
firstrun_continue_to_login=Продолжить
firstrun_skip_login=Пропустить этот шаг
section_menu_action_add_search_engine=Добавить поисковую систему

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

@ -192,6 +192,7 @@ firstrun_form_sub_header=za nadaljevanje v Firefox Sync.
firstrun_email_input_placeholder=E-pošta
firstrun_invalid_input=Zahtevan je veljaven e-poštni naslov
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.

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

@ -173,6 +173,7 @@ section_menu_action_expand_section=ขยายส่วน
section_menu_action_manage_section=จัดการส่วน
section_menu_action_manage_webext=จัดการส่วนขยาย
section_menu_action_add_topsite=เพิ่มไซต์เด่น
section_menu_action_add_search_engine=เพิ่มเครื่องมือค้นหา
section_menu_action_move_up=ย้ายขึ้น
section_menu_action_move_down=ย้ายลง
section_menu_action_privacy_notice=ประกาศความเป็นส่วนตัว
@ -191,6 +192,7 @@ firstrun_form_sub_header=เพื่อดำเนินการต่อไ
firstrun_email_input_placeholder=อีเมล
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_terms_of_service=เงื่อนไขการให้บริการ
@ -198,4 +200,3 @@ firstrun_privacy_notice=ประกาศความเป็นส่วน
firstrun_continue_to_login=ดำเนินการต่อ
firstrun_skip_login=ข้ามขั้นตอนนี้
section_menu_action_add_search_engine=เพิ่มเครื่องมือค้นหา

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

@ -87,18 +87,18 @@ window.gActivityStreamStrings = {
"section_menu_action_manage_section": "সেকশনটি পরিচালনা করুন",
"section_menu_action_manage_webext": "এক্সটেনসন ব্যবহার করুন",
"section_menu_action_add_topsite": "টপ সাইট যোগ করুন",
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ",
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ করুন",
"section_menu_action_move_up": "উপরে উঠাও",
"section_menu_action_move_down": "নীচে নামাও",
"section_menu_action_privacy_notice": "গোপনীয়তা নীতি",
"firstrun_title": "অাপনি Firefox ব্যবহার করুন",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_content": "আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।",
"firstrun_learn_more_link": "Firefox অ্যাকাউন্ট সম্পর্কে আরও জানুন",
"firstrun_form_header": "আপনার ই-মেইল লিখুন",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_form_sub_header": "Firefox সিঙ্ক চালিয়ে যেতে",
"firstrun_email_input_placeholder": "ইমেইল",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_invalid_input": "কার্যকর ইমেইল আবশ্যক",
"firstrun_extra_legal_links": "অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।",
"firstrun_terms_of_service": "সেবার শর্ত",
"firstrun_privacy_notice": "গোপনীয়তা নীতি",
"firstrun_continue_to_login": "চালিয়ে যান",

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

@ -87,18 +87,18 @@ window.gActivityStreamStrings = {
"section_menu_action_manage_section": "সেকশনটি পরিচালনা করুন",
"section_menu_action_manage_webext": "এক্সটেনসন ব্যবহার করুন",
"section_menu_action_add_topsite": "টপ সাইট যোগ করুন",
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ",
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ করুন",
"section_menu_action_move_up": "উপরে উঠাও",
"section_menu_action_move_down": "নীচে নামাও",
"section_menu_action_privacy_notice": "গোপনীয়তা নীতি",
"firstrun_title": "অাপনি Firefox ব্যবহার করুন",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_content": "আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।",
"firstrun_learn_more_link": "Firefox অ্যাকাউন্ট সম্পর্কে আরও জানুন",
"firstrun_form_header": "আপনার ই-মেইল লিখুন",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_form_sub_header": "Firefox সিঙ্ক চালিয়ে যেতে",
"firstrun_email_input_placeholder": "ইমেইল",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_invalid_input": "কার্যকর ইমেইল আবশ্যক",
"firstrun_extra_legal_links": "অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।",
"firstrun_terms_of_service": "সেবার শর্ত",
"firstrun_privacy_notice": "গোপনীয়তা নীতি",
"firstrun_continue_to_login": "চালিয়ে যান",

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

@ -31,7 +31,7 @@ window.gActivityStreamStrings = {
"menu_action_open_file": "Obre el fitxer",
"menu_action_copy_download_link": "Copia l'enllaç de la baixada",
"menu_action_go_to_download_page": "Vés a la pàgina de la baixada",
"menu_action_remove_download": "Suprimeix de l'historial",
"menu_action_remove_download": "Elimina de l'historial",
"search_button": "Cerca",
"search_header": "Cerca de {search_engine_name}",
"search_web_placeholder": "Cerca al web",

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

@ -97,7 +97,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Idatzi zure helbide elektronikoa",
"firstrun_form_sub_header": "Firefox Sync-ekin jarraitzeko.",
"firstrun_email_input_placeholder": "Helbide elektronikoa",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "Baliozko helbide elektronikoa behar da",
"firstrun_extra_legal_links": "Jarraitzearekin bat, {terms} eta {privacy} onartzen dituzu.",
"firstrun_terms_of_service": "Zerbitzu-baldintzak",
"firstrun_privacy_notice": "Pribatutasun-oharra",

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

@ -87,7 +87,7 @@ window.gActivityStreamStrings = {
"section_menu_action_manage_section": "વિભાગ સંચાલિત કરો",
"section_menu_action_manage_webext": "એક્સ્ટેંશનનો વહીવટ કરો",
"section_menu_action_add_topsite": "ટોચની સાઇટ ઉમેરો",
"section_menu_action_add_search_engine": "શોધ યંત્ર ઉમેરો",
"section_menu_action_add_search_engine": "શોધ એંજીન ઉમેરો",
"section_menu_action_move_up": "ઉપર કરો",
"section_menu_action_move_down": "નીચે કરો",
"section_menu_action_privacy_notice": "ખાનગી સૂચના",

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

@ -87,7 +87,7 @@ window.gActivityStreamStrings = {
"section_menu_action_manage_section": "अनुभाग प्रबंधित करें",
"section_menu_action_manage_webext": "विस्तारक प्रबंधित करें",
"section_menu_action_add_topsite": "शीर्ष साइट जोड़ें",
"section_menu_action_add_search_engine": "सर्च इंजन जोड़े",
"section_menu_action_add_search_engine": "खोज ईंजन जोड़ें",
"section_menu_action_move_up": "ऊपर जाएँ",
"section_menu_action_move_down": "नीचे जाएँ",
"section_menu_action_privacy_notice": "गोपनीयता नीति",

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

@ -62,16 +62,16 @@ window.gActivityStreamStrings = {
"topsites_form_edit_header": "რჩეული საიტის ჩასწორება",
"topsites_form_title_label": "დასახელება",
"topsites_form_title_placeholder": "სათაურის შეყვანა",
"topsites_form_url_label": "URL ბმული",
"topsites_form_image_url_label": "სასურველი სურათის URL ბმული",
"topsites_form_url_placeholder": "აკრიფეთ ან ჩასვით URL ბმული",
"topsites_form_url_label": "URL-ბმული",
"topsites_form_image_url_label": "სასურველი სურათის URL-ბმული",
"topsites_form_url_placeholder": "აკრიფეთ ან ჩასვით URL-ბმული",
"topsites_form_use_image_link": "სასურველი სურათის გამოყენება…",
"topsites_form_preview_button": "შეთვალიერება",
"topsites_form_add_button": "დამატება",
"topsites_form_save_button": "შენახვა",
"topsites_form_cancel_button": "გაუქმება",
"topsites_form_url_validation": "საჭიროა მართებული URL",
"topsites_form_image_validation": "სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL ბმული.",
"topsites_form_image_validation": "სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL-ბმული.",
"pocket_read_more": "პოპულარული თემები:",
"pocket_read_even_more": "მეტი სიახლის ნახვა",
"highlights_empty_state": "დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენთვის სასურველი სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.",
@ -93,7 +93,7 @@ window.gActivityStreamStrings = {
"section_menu_action_privacy_notice": "პირადი მონაცემების დაცვის განაცხადი",
"firstrun_title": "თან წაიყოლეთ Firefox",
"firstrun_content": "მიიღეთ წვდომა თქვენს სანიშნებთან, ისტორიასთან, პაროლებსა და სხვა პარამეტრებთან, ყველა თქვენს მოწყობილობაზე.",
"firstrun_learn_more_link": "იხილეთ ვრცლად, Firefox ანგარიშების შესახებ",
"firstrun_learn_more_link": "იხილეთ ვრცლად, Firefox-ანგარიშების შესახებ",
"firstrun_form_header": "შეიყვანეთ თქვენი ელფოსტა",
"firstrun_form_sub_header": "Firefox Sync-ზე გადასასვლელად.",
"firstrun_email_input_placeholder": "ელფოსტა",

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

@ -25,9 +25,9 @@ window.gActivityStreamStrings = {
"menu_action_delete_pocket": "Pocket मधून हटवा",
"menu_action_archive_pocket": "Pocket मध्ये संग्रहित करा",
"menu_action_show_file_mac_os": "Finder मध्ये दर्शवा",
"menu_action_show_file_windows": "Open Containing Folder",
"menu_action_show_file_linux": "Open Containing Folder",
"menu_action_show_file_default": "Show File",
"menu_action_show_file_windows": "समाविष्ट करणारे फोल्डर उघडा",
"menu_action_show_file_linux": "समाविष्ट करणारे फोल्डर उघडा",
"menu_action_show_file_default": "फाईल दाखवा",
"menu_action_open_file": "फाइल उघडा",
"menu_action_copy_download_link": "डाउनलोड दुव्याची प्रत बनवा",
"menu_action_go_to_download_page": "डाउनलोड पृष्ठावर जा",
@ -44,12 +44,12 @@ window.gActivityStreamStrings = {
"prefs_search_header": "वेब शोध",
"prefs_topsites_description": "आपण सर्वाधिक भेट देता त्या साइट",
"prefs_topstories_description2": "आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री",
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
"prefs_topstories_options_sponsored_label": "प्रायोजित कथा",
"prefs_topstories_sponsored_learn_more": "अधिक जाणून घ्या",
"prefs_highlights_description": "आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा",
"prefs_highlights_options_visited_label": "भेट दिलेली पृष्ठे",
"prefs_highlights_options_download_label": "Most Recent Download",
"prefs_highlights_options_pocket_label": "Pages Saved to Pocket",
"prefs_highlights_options_download_label": "अलीकडचे डाउनलोड",
"prefs_highlights_options_pocket_label": "Pocket मध्ये जतन केलेले पृष्ठ",
"prefs_snippets_description": "Mozilla आणि Firefox कडून अद्यतने",
"settings_pane_button_label": "आपले नवीन टॅब पृष्ठ सानुकूलित करा",
"settings_pane_topsites_header": "शीर्ष साइट्स",
@ -63,15 +63,15 @@ window.gActivityStreamStrings = {
"topsites_form_title_label": "शिर्षक",
"topsites_form_title_placeholder": "शिर्षक प्रविष्ट करा",
"topsites_form_url_label": "URL",
"topsites_form_image_url_label": "Custom Image URL",
"topsites_form_image_url_label": "सानुकूल प्रतिमा URL",
"topsites_form_url_placeholder": "URL चिकटवा किंवा टाईप करा",
"topsites_form_use_image_link": "Use a custom image…",
"topsites_form_use_image_link": "सानुकूल प्रतिमा वापरा…",
"topsites_form_preview_button": "पूर्वावलोकन",
"topsites_form_add_button": "समाविष्ट करा",
"topsites_form_save_button": "जतन करा",
"topsites_form_cancel_button": "रद्द करा",
"topsites_form_url_validation": "वैध URL आवश्यक",
"topsites_form_image_validation": "Image failed to load. Try a different URL.",
"topsites_form_image_validation": "प्रतिमा लोड झाली नाही. वेगळी URL वापरून पहा.",
"pocket_read_more": "लोकप्रिय विषय:",
"pocket_read_even_more": "अधिक कथा पहा",
"highlights_empty_state": "ब्राउझिंग सुरू करा, आणि आम्ही आपल्याला इथे आपण अलीकडील भेट दिलेले किंवा वाचनखूण लावलेले उत्कृष्ठ लेख, व्हिडिओ, आणि इतर पृष्ठांपैकी काही दाखवू.",
@ -79,26 +79,26 @@ window.gActivityStreamStrings = {
"manual_migration_explanation2": "दुसऱ्या ब्राऊझरमधील वाचनखूणा, इतिहास आणि पासवर्ड सोबत Firefox ला वापरून पहा.",
"manual_migration_cancel_button": "नाही धन्यवाद",
"manual_migration_import_button": "आता आयात करा",
"error_fallback_default_info": "Oops, something went wrong loading this content.",
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
"error_fallback_default_info": "अरेरे, हा मजकूर लोड करताना काहीतरी गोंधळ झाला.",
"error_fallback_default_refresh_suggestion": "पुन्हा प्रयत्न करण्यासाठी पृष्ठ रिफ्रेश करा.",
"section_menu_action_remove_section": "विभाग काढा",
"section_menu_action_collapse_section": "विभाग ढासळा",
"section_menu_action_expand_section": "Expand Section",
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_expand_section": "विभाग वाढवा",
"section_menu_action_manage_section": "विभाग व्यवस्थापित करा",
"section_menu_action_manage_webext": "एक्सटेन्शन व्यवस्थापित करा",
"section_menu_action_add_topsite": "Add Top Site",
"section_menu_action_add_topsite": "खास साईट्स जोडा",
"section_menu_action_add_search_engine": "शोध इंजीन जोडा",
"section_menu_action_move_up": "वर जा",
"section_menu_action_move_down": "खाली जा",
"section_menu_action_privacy_notice": "गोपनीयता सूचना",
"firstrun_title": "Firefox सोबत न्या",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_content": "आपले बुकमार्क्स, इतिहास, पासवर्ड आणि इतर सेटिंग आपल्या सर्व उपकरणांवर मिळवा.",
"firstrun_learn_more_link": "Firefox खात्यांविषयी अधिक जाणून घ्या",
"firstrun_form_header": "ईमेल प्रविष्ट करा",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_form_sub_header": "Firefox Sync वर सुरू ठेवण्यासाठी",
"firstrun_email_input_placeholder": "ईमेल",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_invalid_input": "वैध ईमेल आवश्यक",
"firstrun_extra_legal_links": "पुढे जाताना आपण {terms} आणि {privacy} यांना संमती देता.",
"firstrun_terms_of_service": "सेवा अटी",
"firstrun_privacy_notice": "गोपनीयता सूचना",
"firstrun_continue_to_login": "पुढे चला",

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

@ -97,7 +97,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Vnesite e-poštni naslov",
"firstrun_form_sub_header": "za nadaljevanje v Firefox Sync.",
"firstrun_email_input_placeholder": "E-pošta",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "Zahtevan je veljaven e-poštni naslov",
"firstrun_extra_legal_links": "Z nadaljevanjem se strinjate s {terms} in {privacy}.",
"firstrun_terms_of_service": "Pogoji uporabe",
"firstrun_privacy_notice": "Obvestilom o zasebnosti",

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

@ -56,5 +56,8 @@ window.gActivityStreamPrerenderedState = {
"order": 2,
"initialized": false
}
]
],
"Pocket": {
"waitingForSpoc": true
}
};

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

@ -110,6 +110,16 @@ add_task(async function checkProfileAgeReset() {
"should select correct item by profile age reset");
});
add_task(async function checkCurrentDate() {
let message = {id: "foo", targeting: `currentDate < '${new Date(Date.now() + 1000)}'|date`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select message based on currentDate < timestamp");
message = {id: "foo", targeting: `currentDate > '${new Date(Date.now() - 1000)}'|date`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select message based on currentDate > timestamp");
});
add_task(async function checkhasFxAccount() {
await pushPrefs(["services.sync.username", "someone@foo.com"]);
is(await ASRouterTargeting.Environment.hasFxAccount, true,
@ -291,3 +301,12 @@ add_task(async function check_sync() {
is(await ASRouterTargeting.Environment.sync.totalDevices, Services.prefs.getIntPref("services.sync.numClients", 0),
"should return correct mobileDevices info");
});
add_task(async function check_onboarding_cohort() {
Services.prefs.setStringPref("browser.newtabpage.activity-stream.asrouter.messageProviders", JSON.stringify([{id: "onboarding", enabled: true, cohort: 1}]));
is(await ASRouterTargeting.Environment.isInExperimentCohort, 1);
Services.prefs.setStringPref("browser.newtabpage.activity-stream.asrouter.messageProviders", JSON.stringify(17));
is(await ASRouterTargeting.Environment.isInExperimentCohort, 0);
Services.prefs.setStringPref("browser.newtabpage.activity-stream.asrouter.messageProviders", JSON.stringify([{id: "onboarding", enabled: true, cohort: "hello"}]));
is(await ASRouterTargeting.Environment.isInExperimentCohort, 0);
});

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

@ -0,0 +1,36 @@
import {combineReducers, createStore} from "redux";
import {actionTypes as at} from "common/Actions.jsm";
import {enableASRouterContent} from "content-src/lib/asroutercontent.js";
import {reducers} from "common/Reducers.jsm";
describe("asrouter", () => {
let sandbox;
let store;
let asrouterContent;
beforeEach(() => {
sandbox = sinon.sandbox.create();
store = createStore(combineReducers(reducers));
sandbox.spy(store, "subscribe");
});
it("should initialize asrouter once if asrouterExperimentEnabled is true", () => {
({asrouterContent} = enableASRouterContent(store, {
init: sandbox.stub(),
uninit: sandbox.stub(),
initialized: false
}));
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
assert.calledOnce(asrouterContent.init);
});
it("should uninitialize asrouter if asrouterExperimentEnabled pref is turned off", () => {
({asrouterContent} = enableASRouterContent(store, {
init: sandbox.stub(),
uninit: sandbox.stub(),
initialized: true
}));
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: false}});
assert.calledOnce(asrouterContent.uninit);
});
});

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

@ -6,15 +6,17 @@ import {
FAKE_LOCAL_PROVIDERS,
FAKE_REMOTE_MESSAGES,
FAKE_REMOTE_PROVIDER,
FAKE_REMOTE_SETTINGS_PROVIDER,
FakeRemotePageManager,
PARENT_TO_CHILD_MESSAGE_NAME
} from "./constants";
import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
import {CFRPageActions} from "lib/CFRPageActions.jsm";
import {GlobalOverrider} from "test/unit/utils";
import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
const MESSAGE_PROVIDER_PREF_NAME = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER];
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER, FAKE_REMOTE_SETTINGS_PROVIDER];
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
const ONE_DAY = 24 * 60 * 60 * 1000;
@ -33,8 +35,10 @@ describe("ASRouter", () => {
let Router;
let channel;
let sandbox;
let blockList;
let impressions;
let messageBlockList;
let providerBlockList;
let messageImpressions;
let providerImpressions;
let fetchStub;
let clock;
let getStringPrefStub;
@ -43,8 +47,10 @@ describe("ASRouter", () => {
function createFakeStorage() {
const getStub = sandbox.stub();
getStub.withArgs("blockList").returns(Promise.resolve(blockList));
getStub.withArgs("impressions").returns(Promise.resolve(impressions));
getStub.withArgs("messageBlockList").returns(Promise.resolve(messageBlockList));
getStub.withArgs("providerBlockList").returns(Promise.resolve(providerBlockList));
getStub.withArgs("messageImpressions").returns(Promise.resolve(messageImpressions));
getStub.withArgs("providerImpressions").returns(Promise.resolve(providerImpressions));
return {
get: getStub,
set: sandbox.stub().returns(Promise.resolve())
@ -66,8 +72,10 @@ describe("ASRouter", () => {
}
beforeEach(async () => {
blockList = [];
impressions = {};
messageBlockList = [];
providerBlockList = [];
messageImpressions = {};
providerImpressions = {};
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
fetchStub = sandbox.stub(global, "fetch")
@ -100,23 +108,23 @@ describe("ASRouter", () => {
assert.calledOnce(addObserverStub);
assert.calledWith(addObserverStub, MESSAGE_PROVIDER_PREF_NAME);
});
it("should set state.blockList to the block list in persistent storage", async () => {
blockList = ["foo"];
it("should set state.messageBlockList to the block list in persistent storage", async () => {
messageBlockList = ["foo"];
Router = new _ASRouter({providers: FAKE_PROVIDERS});
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.deepEqual(Router.state.blockList, ["foo"]);
assert.deepEqual(Router.state.messageBlockList, ["foo"]);
});
it("should set state.impressions to the impressions object in persistent storage", async () => {
// Note that impressions are only kept if a message exists in router and has a .frequency property,
it("should set state.messageImpressions to the messageImpressions object in persistent storage", async () => {
// Note that messageImpressions are only kept if a message exists in router and has a .frequency property,
// otherwise they will be cleaned up by .cleanupImpressions()
const testMessage = {id: "foo", frequency: {lifetimeCap: 10}};
impressions = {foo: [0, 1, 2]};
messageImpressions = {foo: [0, 1, 2]};
Router = new _ASRouter({providers: [{id: "onboarding", type: "local", messages: [testMessage]}]});
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.deepEqual(Router.state.impressions, impressions);
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
});
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
Router = new _ASRouter(MESSAGE_PROVIDER_PREF_NAME, FAKE_LOCAL_PROVIDERS);
@ -137,7 +145,7 @@ describe("ASRouter", () => {
});
it("should update the list of providers on pref change", async () => {
const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {url: "baz.com"});
setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider]);
setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider, FAKE_REMOTE_SETTINGS_PROVIDER]);
const {length} = Router.state.providers;
await Router.observe("", "", MESSAGE_PROVIDER_PREF_NAME);
@ -177,7 +185,7 @@ describe("ASRouter", () => {
it("should not trigger an update if not enough time has passed for a provider", async () => {
await createRouterAndInit([
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300}
{id: "remotey", type: "remote", enabled: true, url: "http://fake.com/endpoint", updateCycleInMs: 300}
]);
const previousState = Router.state;
@ -189,7 +197,7 @@ describe("ASRouter", () => {
});
it("should not trigger an update if we only have local providers", async () => {
await createRouterAndInit([
{id: "foo", type: "local", messages: FAKE_LOCAL_MESSAGES}
{id: "foo", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES}
]);
const previousState = Router.state;
@ -202,8 +210,8 @@ describe("ASRouter", () => {
it("should update messages for a provider if enough time has passed, without removing messages for other providers", async () => {
const NEW_MESSAGES = [{id: "new_123"}];
await createRouterAndInit([
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300},
{id: "alocalprovider", type: "local", messages: FAKE_LOCAL_MESSAGES}
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", enabled: true, updateCycleInMs: 300},
{id: "alocalprovider", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES}
]);
fetchStub
.withArgs("http://fake.com/endpoint")
@ -222,7 +230,7 @@ describe("ASRouter", () => {
/* eslint-disable object-curly-newline */ /* eslint-disable object-property-newline */
await createRouterAndInit([
{id: "foo", type: "local", messages: [
{id: "foo", type: "local", enabled: true, messages: [
{id: "foo", template: "simple_template", trigger: {id: "firstRun"}, content: {title: "Foo", body: "Foo123"}},
{id: "bar1", template: "simple_template", trigger: {id: "openURL", params: ["www.mozilla.org", "www.mozilla.com"]}, content: {title: "Bar1", body: "Bar123"}},
{id: "bar2", template: "simple_template", trigger: {id: "openURL", params: ["www.example.com"]}, content: {title: "Bar2", body: "Bar123"}}
@ -236,6 +244,10 @@ describe("ASRouter", () => {
assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
Router._triggerHandler, ["www.example.com"]);
});
it("should gracefully handle RemoteSettings blowing up", async () => {
sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").rejects("fake error");
await createRouterAndInit();
});
});
describe("#_updateMessageProviders", () => {
@ -243,7 +255,7 @@ describe("ASRouter", () => {
// If this test fails, you need to update the constant STARTPAGE_VERSION in
// ASRouter.jsm to match the `version` property of provider-response-schema.json
const expectedStartpageVersion = ProviderResponseSchema.version;
const provider = {id: "foo", type: "remote", url: "https://www.mozilla.org/%STARTPAGE_VERSION%/"};
const provider = {id: "foo", enabled: true, type: "remote", url: "https://www.mozilla.org/%STARTPAGE_VERSION%/"};
setMessageProviderPref([provider]);
Router._updateMessageProviders();
assert.equal(Router.state.providers[0].url, `https://www.mozilla.org/${expectedStartpageVersion}/`);
@ -254,19 +266,29 @@ describe("ASRouter", () => {
const stub = sandbox.stub(global.Services.urlFormatter, "formatURL")
.withArgs(url)
.returns(replacedUrl);
const provider = {id: "foo", type: "remote", url};
const provider = {id: "foo", enabled: true, type: "remote", url};
setMessageProviderPref([provider]);
Router._updateMessageProviders();
assert.calledOnce(stub);
assert.calledWithExactly(stub, url);
assert.equal(Router.state.providers[0].url, replacedUrl);
});
it("should only add the providers that are enabled", () => {
const providers = [
{id: "foo", enabled: false, type: "remote", url: "https://www.foo.com/"},
{id: "bar", enabled: true, type: "remote", url: "https://www.bar.com/"}
];
setMessageProviderPref(providers);
Router._updateMessageProviders();
assert.equal(Router.state.providers.length, 1);
assert.equal(Router.state.providers[0].id, providers[1].id);
});
});
describe("blocking", () => {
it("should not return a blocked message", async () => {
// Block all messages except the first
await Router.setState(() => ({blockList: ALL_MESSAGE_IDS.slice(1)}));
await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS.slice(1)}));
const targetStub = {sendAsyncMessage: sandbox.stub()};
await Router.sendNextMessage(targetStub);
@ -274,8 +296,19 @@ describe("ASRouter", () => {
assert.calledOnce(targetStub.sendAsyncMessage);
assert.equal(Router.state.lastMessageId, ALL_MESSAGE_IDS[0]);
});
it("should not return a message from a blocked provider", async () => {
// There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
// only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
await Router.setState(() => ({providerBlockList: [FAKE_LOCAL_PROVIDER.id]}));
const targetStub = {sendAsyncMessage: sandbox.stub()};
await Router.sendNextMessage(targetStub);
assert.calledOnce(targetStub.sendAsyncMessage);
assert.equal(Router.state.lastMessageId, FAKE_REMOTE_MESSAGES[0].id);
});
it("should not return a message if all messages are blocked", async () => {
await Router.setState(() => ({blockList: ALL_MESSAGE_IDS}));
await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS}));
const targetStub = {sendAsyncMessage: sandbox.stub()};
await Router.sendNextMessage(targetStub);
@ -400,59 +433,84 @@ describe("ASRouter", () => {
});
describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
it("should add the id to the blockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
await Router.setState({lastMessageId: "foo"});
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}});
await Router.onMessage(msg);
assert.isTrue(Router.state.blockList.includes("foo"));
assert.isTrue(Router.state.messageBlockList.includes("foo"));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
});
});
describe("#onMessage: BLOCK_PROVIDER_BY_ID", () => {
it("should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id", async () => {
const msg = fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "bar"}});
await Router.onMessage(msg);
assert.isTrue(Router.state.providerBlockList.includes("bar"));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: "bar"}});
});
});
describe("#onMessage: BLOCK_BUNDLE", () => {
it("should add all the ids in the bundle to the blockList and send a CLEAR_BUNDLE message", async () => {
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
const bundleIds = [FAKE_BUNDLE[0].id, FAKE_BUNDLE[1].id];
await Router.setState({lastMessageId: "foo"});
const msg = fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}});
await Router.onMessage(msg);
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
assert.calledWithExactly(Router._storage.set, "blockList", bundleIds);
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
});
});
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
it("should remove the id from the blockList", async () => {
it("should remove the id from the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.blockList.includes("foo"));
assert.isTrue(Router.state.messageBlockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.blockList.includes("foo"));
assert.isFalse(Router.state.messageBlockList.includes("foo"));
});
it("should save the blockList", async () => {
it("should save the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.calledWithExactly(Router._storage.set, "blockList", []);
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
describe("#onMessage: UNBLOCK_PROVIDER_BY_ID", () => {
it("should remove the id from the providerBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.providerBlockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.providerBlockList.includes("foo"));
});
it("should save the providerBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
assert.calledWithExactly(Router._storage.set, "providerBlockList", []);
});
});
describe("#onMessage: UNBLOCK_BUNDLE", () => {
it("should remove all the ids in the bundle from the blockList", async () => {
it("should remove all the ids in the bundle from the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isFalse(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isFalse(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
});
it("should save the blockList", async () => {
it("should save the messageBlockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.calledWithExactly(Router._storage.set, "blockList", []);
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
});
});
@ -548,15 +606,15 @@ describe("ASRouter", () => {
await Router.onMessage(msg);
assert.calledOnce(Router._findMessage);
assert.deepEqual(Router._findMessage.firstCall.args[2], {id: "firstRun"});
assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
});
it("consider the trigger when picking a message", async () => {
let messages = [
const messages = [
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}}
];
const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
let message = await Router._findMessage(messages, target, {id: "foo"});
const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
const message = await Router._findMessage(messages, data.data.trigger);
assert.equal(message, messages[0]);
});
it("should pick a message with the right targeting and trigger", async () => {
@ -583,6 +641,17 @@ describe("ASRouter", () => {
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
});
it("should call CFRPageActions.addRecommendation if the template is cfr_action", async () => {
sandbox.stub(CFRPageActions, "addRecommendation");
const testMessage = {id: "foo", template: "cfr_doorhanger"};
await Router.setState({messages: [testMessage]});
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
await Router.onMessage(msg);
assert.notCalled(msg.target.sendAsyncMessage);
assert.calledOnce(CFRPageActions.addRecommendation);
});
it("should broadcast CLEAR_ALL if provided id did not resolve to a message", async () => {
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: -1}});
await Router.onMessage(msg);
@ -600,24 +669,23 @@ describe("ASRouter", () => {
assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {private: true});
});
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
sinon.spy(Router, "openLinkIn");
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "OPEN_URL", data: {url: "some/url.com"}};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledWith(Router.openLinkIn, "some/url.com", msg.target, {isPrivate: false, where: "tabshifted"});
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
"some/url.com", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
});
it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
sinon.spy(Router, "openLinkIn");
let [testMessage] = Router.state.messages;
testMessage.button_action = {type: "OPEN_ABOUT_PAGE", data: {page: "something"}};
const msg = fakeExecuteUserAction(testMessage.button_action);
await Router.onMessage(msg);
assert.calledWith(Router.openLinkIn, `about:something`, msg.target, {isPrivate: false, trusted: true, where: "tab"});
assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
assert.calledWith(msg.target.browser.ownerGlobal.openTrustedLinkIn, "about:something", "tab");
});
});
@ -633,6 +701,21 @@ describe("ASRouter", () => {
});
});
describe("#dispatch(action, target)", () => {
it("should an action and target to onMessage", async () => {
// use the IMPRESSION action to make sure actions are actually getting processed
sandbox.stub(Router, "addImpression");
sandbox.spy(Router, "onMessage");
const target = {};
const action = {type: "IMPRESSION"};
Router.dispatch(action, target);
assert.calledWith(Router.onMessage, {data: action, target});
assert.calledOnce(Router.addImpression);
});
});
describe("_triggerHandler", () => {
it("should call #onMessage with the correct trigger", () => {
sinon.spy(Router, "onMessage");
@ -640,7 +723,7 @@ describe("ASRouter", () => {
const trigger = {id: "FAKE_TRIGGER", param: "some fake param"};
Router._triggerHandler(target, trigger);
assert.calledOnce(Router.onMessage);
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", trigger}});
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", data: {trigger}}});
});
});
@ -671,26 +754,181 @@ describe("ASRouter", () => {
});
describe("impressions", () => {
it("should add an impression and update _storage with the current time if the message frequency caps", async () => {
clock.tick(42);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", frequency: {lifetime: 5}}});
await Router.onMessage(msg);
async function addProviderWithFrequency(id, frequency) {
await Router.setState(state => {
const newProvider = {id, frequency};
const providers = [...state.providers, newProvider];
return {providers};
});
}
assert.isArray(Router.state.impressions.foo);
assert.deepEqual(Router.state.impressions.foo, [42]);
assert.calledWith(Router._storage.set, "impressions", {foo: [42]});
describe("#addImpression", () => {
it("should add a message impression and update _storage with the current time if the message has frequency caps", async () => {
clock.tick(42);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", provider: FAKE_LOCAL_PROVIDER.id, frequency: {lifetime: 5}}});
await Router.onMessage(msg);
assert.isArray(Router.state.messageImpressions.foo);
assert.deepEqual(Router.state.messageImpressions.foo, [42]);
assert.calledWith(Router._storage.set, "messageImpressions", {foo: [42]});
});
it("should not add a message impression if the message doesn't have frequency caps", async () => {
// Note that storage.set is called during initialization, so it needs to be reset
Router._storage.set.reset();
clock.tick(42);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo"}});
await Router.onMessage(msg);
assert.notProperty(Router.state.messageImpressions, "foo");
assert.notCalled(Router._storage.set);
});
it("should add a provider impression and update _storage with the current time if the message's provider has frequency caps", async () => {
clock.tick(42);
await addProviderWithFrequency("foo", {lifetime: 5});
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "bar", provider: "foo"}});
await Router.onMessage(msg);
assert.isArray(Router.state.providerImpressions.foo);
assert.deepEqual(Router.state.providerImpressions.foo, [42]);
assert.calledWith(Router._storage.set, "providerImpressions", {foo: [42]});
});
it("should not add a provider impression if the message's provider doesn't have frequency caps", async () => {
// Note that storage.set is called during initialization, so it needs to be reset
Router._storage.set.reset();
clock.tick(42);
// Add "foo" provider with no frequency
await addProviderWithFrequency("foo", null);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "bar", provider: "foo"}});
await Router.onMessage(msg);
assert.notProperty(Router.state.providerImpressions, "foo");
assert.notCalled(Router._storage.set);
});
});
it("should not add an impression if the message doesn't have frequency caps", async () => {
// Note that storage.set is called during initialization, so it needs to be reset
Router._storage.set.reset();
clock.tick(42);
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo"}});
await Router.onMessage(msg);
assert.notProperty(Router.state.impressions, "foo");
assert.notCalled(Router._storage.set);
describe("#isBelowFrequencyCaps", () => {
it("should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments", async () => {
sinon.spy(Router, "_isBelowItemFrequencyCap");
const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter
const fooMessageImpressions = [0, 1];
const barProviderImpressions = [0, 1, 2];
const message = {id: "foo", provider: "bar", frequency: {lifetime: 3}};
const provider = {id: "bar", frequency: {lifetime: 5}};
await Router.setState(state => {
// Add provider
const providers = [...state.providers, provider];
// Add fooMessageImpressions
const messageImpressions = Object.assign({}, state.messageImpressions); // eslint-disable-line no-shadow
messageImpressions.foo = fooMessageImpressions;
// Add barProviderImpressions
const providerImpressions = Object.assign({}, state.providerImpressions); // eslint-disable-line no-shadow
providerImpressions.bar = barProviderImpressions;
return {providers, messageImpressions, providerImpressions};
});
await Router.isBelowFrequencyCaps(message);
assert.calledTwice(Router._isBelowItemFrequencyCap);
assert.calledWithExactly(Router._isBelowItemFrequencyCap, message, fooMessageImpressions, MAX_MESSAGE_LIFETIME_CAP);
assert.calledWithExactly(Router._isBelowItemFrequencyCap, provider, barProviderImpressions);
});
});
describe("getLongestPeriod", () => {
describe("#_isBelowItemFrequencyCap", () => {
it("should return false if the # of impressions exceeds the maxLifetimeCap", () => {
const item = {id: "foo", frequency: {lifetime: 5}};
const impressions = [0, 1];
const maxLifetimeCap = 1;
const result = Router._isBelowItemFrequencyCap(item, impressions, maxLifetimeCap);
assert.isFalse(result);
});
describe("lifetime frequency caps", () => {
it("should return true if .frequency is not defined on the item", () => {
const item = {id: "foo"};
const impressions = [0, 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return true if there are no impressions", () => {
const item = {id: "foo", frequency: {lifetime: 10, custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return true if the # of impressions is less than .frequency.lifetime of the item", () => {
const item = {id: "foo", frequency: {lifetime: 3}};
const impressions = [0, 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return false if the # of impressions is equal to .frequency.lifetime of the item", async () => {
const item = {id: "foo", frequency: {lifetime: 3}};
const impressions = [0, 1, 2];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should return false if the # of impressions is greater than .frequency.lifetime of the item", async () => {
const item = {id: "foo", frequency: {lifetime: 3}};
const impressions = [0, 1, 2, 3];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
});
describe("custom frequency caps", () => {
it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const item = {id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
const impressions = [0, ONE_DAY + 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
clock.tick(200);
const item = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}], lifetime: 3}};
const impressions = [0, 160, 161];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY + 200);
const itemTrue = {id: "msg2", frequency: {custom: [{period: 100, cap: 2}]}};
const itemFalse = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}, {period: ONE_DAY, cap: 3}]}};
const impressions = [0, ONE_DAY + 160, ONE_DAY - 100, ONE_DAY - 200];
assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));
assert.isFalse(Router._isBelowItemFrequencyCap(itemFalse, impressions));
});
it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isTrue(result);
});
it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3];
const result = Router._isBelowItemFrequencyCap(item, impressions);
assert.isFalse(result);
});
it("should allow the 'daily' alias for period", () => {
clock.tick(ONE_DAY + 10);
const item = {id: "msg1", frequency: {custom: [{period: "daily", cap: 2}]}};
assert.isFalse(Router._isBelowItemFrequencyCap(item, [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3]));
assert.isTrue(Router._isBelowItemFrequencyCap(item, [0, 1, 2, 3, ONE_DAY + 1]));
});
});
});
describe("#getLongestPeriod", () => {
it("should return the period if there is only one definition", () => {
const message = {id: "foo", frequency: {custom: [{period: 200, cap: 2}]}};
assert.equal(Router.getLongestPeriod(message), 200);
@ -708,58 +946,59 @@ describe("ASRouter", () => {
assert.isNull(Router.getLongestPeriod(message));
});
});
describe("cleanup on init", () => {
it("should clear impressions for messages which do not exist in state.messages", async () => {
it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}];
impressions = {foo: [0], bar: [0, 1]};
messageImpressions = {foo: [0], bar: [0, 1]};
// Impressions for "bar" should be removed since that id does not exist in messages
const result = {foo: [0]};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should clear impressions older than the period if no lifetime impression cap is included", async () => {
it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
const CURRENT_TIME = ONE_DAY * 2;
clock.tick(CURRENT_TIME);
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}]}}];
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
const result = {foo: [CURRENT_TIME - 10]};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should clear impressions older than the longest period if no lifetime impression cap is included", async () => {
it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
const CURRENT_TIME = ONE_DAY * 2;
clock.tick(CURRENT_TIME);
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}, {period: 100, cap: 2}]}}];
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
const result = {foo: [CURRENT_TIME - 10]};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should clear impressions if they are not properly formatted", async () => {
it("should clear messageImpressions if they are not properly formatted", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}];
// this is impromperly formatted since impressions are supposed to be an array
impressions = {foo: 0};
// this is impromperly formatted since messageImpressions are supposed to be an array
messageImpressions = {foo: 0};
const result = {};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
});
it("should not clear impressions for messages which do exist in state.messages", async () => {
it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}, {id: "bar", frequency: {lifetime: 10}}];
impressions = {foo: [0], bar: []};
messageImpressions = {foo: [0], bar: []};
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
assert.notCalled(Router._storage.set);
assert.deepEqual(Router.state.impressions, impressions);
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
});
});
});

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

@ -1,100 +1,27 @@
import {ASRouterTargeting} from "lib/ASRouterTargeting.jsm";
const ONE_DAY = 24 * 60 * 60 * 1000;
// Note that tests for the ASRouterTargeting environment can be found in
// test/functional/mochitest/browser_asrouter_targeting.js
describe("ASRouterTargeting#isBelowFrequencyCap", () => {
describe("lifetime frequency caps", () => {
it("should return true if .frequency is not defined on the message", () => {
const message = {id: "msg1"};
const impressions = [0, 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return true if there are no impressions", () => {
const message = {id: "msg1", frequency: {lifetime: 10, custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return true if the # of impressions is less than .frequency.lifetime", () => {
const message = {id: "msg1", frequency: {lifetime: 3}};
const impressions = [0, 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return false if the # of impressions is equal to .frequency.lifetime", () => {
const message = {id: "msg1", frequency: {lifetime: 2}};
const impressions = [0, 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should return false if the # of impressions is greater than .frequency.lifetime", () => {
const message = {id: "msg1", frequency: {lifetime: 2}};
const impressions = [0, 1, 2];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
describe("ASRouterTargeting#isInExperimentCohort", () => {
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
});
describe("custom frequency caps", () => {
let sandbox;
let clock;
beforeEach(() => {
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
});
afterEach(() => {
sandbox.restore();
});
it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
const impressions = [0, ONE_DAY + 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
clock.tick(200);
const message = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}], lifetime: 3}};
const impressions = [0, 160, 161];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
clock.tick(ONE_DAY + 200);
const messageTrue = {id: "msg2", frequency: {custom: [{period: 100, cap: 2}]}};
const messageFalse = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}, {period: ONE_DAY, cap: 3}]}};
const impressions = [0, ONE_DAY + 160, ONE_DAY - 100, ONE_DAY - 200];
assert.isTrue(ASRouterTargeting.isBelowFrequencyCap(messageTrue, impressions));
assert.isFalse(ASRouterTargeting.isBelowFrequencyCap(messageFalse, impressions));
});
it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isTrue(result);
});
it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
const impressions = [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3];
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
assert.isFalse(result);
});
it("should allow the 'daily' alias for period", () => {
clock.tick(ONE_DAY + 10);
const message = {id: "msg1", frequency: {custom: [{period: "daily", cap: 2}]}};
assert.isFalse(ASRouterTargeting.isBelowFrequencyCap(message, [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3]));
assert.isTrue(ASRouterTargeting.isBelowFrequencyCap(message, [0, 1, 2, 3, ONE_DAY + 1]));
});
afterEach(() => sandbox.restore());
it("should return the correct if the onboardingCohort pref value", () => {
sandbox.stub(global.Services.prefs, "getStringPref").returns(JSON.stringify([{id: "onboarding", cohort: 1}]));
const result = ASRouterTargeting.Environment.isInExperimentCohort;
assert.equal(result, 1);
});
it("should return 0 if it cannot find the pref", () => {
sandbox.stub(global.Services.prefs, "getStringPref").returns("");
const result = ASRouterTargeting.Environment.isInExperimentCohort;
assert.equal(result, 0);
});
it("should return 0 if it fails to parse the pref", () => {
sandbox.stub(global.Services.prefs, "getStringPref").returns(17);
const result = ASRouterTargeting.Environment.isInExperimentCohort;
assert.equal(result, 0);
});
});

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

@ -13,11 +13,7 @@ describe("ASRouterTriggerListeners", () => {
function resetEnumeratorStub(windows) {
windowEnumeratorStub
.withArgs("navigator:browser")
.returns({
_count: -1,
hasMoreElements() { this._count++; return this._count < windows.length; },
getNext() { return windows[this._count]; }
});
.returns(windows);
}
beforeEach(async () => {
@ -121,12 +117,12 @@ describe("ASRouterTriggerListeners", () => {
const newTriggerHandler = sinon.stub();
openURLListener.init(newTriggerHandler, hosts);
const browser = {messageManager: {}};
const browser = {};
const webProgress = {isTopLevel: true};
const location = "https://www.mozilla.org/something";
openURLListener.onLocationChange(browser, webProgress, undefined, {spec: location});
assert.calledOnce(newTriggerHandler);
assert.calledWithExactly(newTriggerHandler, browser.messageManager, {id: "openURL", param: "www.mozilla.org"});
assert.calledWithExactly(newTriggerHandler, browser, {id: "openURL", param: "www.mozilla.org"});
});
});
});

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

@ -9,13 +9,15 @@ export const FAKE_LOCAL_MESSAGES = [
{id: "bar", template: "fancy_template", content: {title: "Foo", body: "Foo123"}},
{id: "baz", content: {title: "Foo", body: "Foo123"}}
];
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", localProvider: "FAKE_LOCAL_PROVIDER"};
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", localProvider: "FAKE_LOCAL_PROVIDER", enabled: true, cohort: 0};
export const FAKE_LOCAL_PROVIDERS = {FAKE_LOCAL_PROVIDER: {getMessages: () => FAKE_LOCAL_MESSAGES}};
export const FAKE_REMOTE_MESSAGES = [
{id: "qux", template: "simple_template", content: {title: "Qux", body: "hello world"}}
];
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint"};
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint", enabled: true};
export const FAKE_REMOTE_SETTINGS_PROVIDER = {id: "remotey-settingsy", type: "remote-settings", bucket: "bucketname", enabled: true};
// Stubs methods on RemotePageManager
export class FakeRemotePageManager {

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

@ -0,0 +1,41 @@
import {CFRMessageProvider} from "lib/CFRMessageProvider.jsm";
import schema from "content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json";
const DEFAULT_CONTENT = {
"notification_text": "Recommendation",
"heading_text": "Recommended Extension",
"info_icon": {
"label": "why_seeing_this",
"sumo_path": "extensionrecommendations"
},
"addon": {
"title": "Addon name",
"icon": "https://mozilla.org/icon",
"author": "Author name",
"amo_url": "https://example.com"
},
"text": "Description of addon",
"buttons": {
"primary": {
"label": "btn_ok",
"action": {
"type": "INSTALL_ADDON_FROM_URL",
"data": {"url": "https://example.com"}
}
},
"secondary": {
"label": "btn_cancel",
"action": {"type": "CANCEL"}
}
}
};
describe("ExtensionDoorhanger", () => {
it("should validate DEFAULT_CONTENT", () => {
assert.jsonSchema(DEFAULT_CONTENT, schema);
});
it("should validate all messages from CFRMessageProvider", () => {
const messages = CFRMessageProvider.getMessages();
messages.forEach(msg => assert.jsonSchema(msg.content, schema));
});
});

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

@ -1,5 +1,5 @@
import {INITIAL_STATE, insertPinned, reducers} from "common/Reducers.jsm";
const {TopSites, App, Snippets, Prefs, Dialog, Sections} = reducers;
const {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket} = reducers;
import {actionTypes as at} from "common/Actions.jsm";
describe("Reducers", () => {
@ -601,4 +601,13 @@ describe("Reducers", () => {
assert.deepEqual(state.blockList, []);
});
});
describe("Pocket", () => {
it("should return INITIAL_STATE by default", () => {
assert.equal(Pocket(undefined, {type: "some_action"}), INITIAL_STATE.Pocket);
});
it("should set waitingForSpoc on a POCKET_WAITING_FOR_SPOC action", () => {
const state = Pocket(undefined, {type: at.POCKET_WAITING_FOR_SPOC, data: false});
assert.isFalse(state.waitingForSpoc);
});
});
});

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

@ -15,6 +15,11 @@ function mountSectionWithProps(props) {
return mountWithIntl(<Provider store={store}><Section {...props} /></Provider>);
}
function mountSectionIntlWithProps(props) {
const store = createStore(combineReducers(reducers), INITIAL_STATE);
return mountWithIntl(<Provider store={store}><SectionIntl {...props} /></Provider>);
}
describe("<Sections>", () => {
let wrapper;
let FAKE_SECTIONS;
@ -176,21 +181,33 @@ describe("<Section>", () => {
};
});
it("should not render for empty topics", () => {
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
assert.lengthOf(wrapper.find(".topic"), 0);
});
it("should render for non-empty topics", () => {
TOP_STORIES_SECTION.topics = [{name: "topic1", url: "topic-url1"}];
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
assert.lengthOf(wrapper.find(".topic"), 1);
});
it("should delay render of third rec to give time for potential spoc", async () => {
TOP_STORIES_SECTION.rows = [
{guid: 1, link: "http://localhost"},
{guid: 2, link: "http://localhost"},
{guid: 3, link: "http://localhost"}
];
wrapper = shallow(<Section Pocket={{waitingForSpoc: true}} {...TOP_STORIES_SECTION} />);
assert.lengthOf(wrapper.find(PlaceholderCard), 1);
wrapper.setProps({Pocket: {waitingForSpoc: false}});
assert.lengthOf(wrapper.find(PlaceholderCard), 0);
});
it("should render for uninitialized topics", () => {
delete TOP_STORIES_SECTION.topics;
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
assert.lengthOf(wrapper.find(".topic"), 1);
});

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

@ -445,7 +445,6 @@ describe("addSnippetsSubscriber", () => {
let store;
let sandbox;
let snippets;
let asrouterContent;
function setSnippetEnabledPref(value) {
store.dispatch({type: at.PREF_CHANGED, data: {name: "feeds.snippets", value}});
}
@ -454,13 +453,7 @@ describe("addSnippetsSubscriber", () => {
store = createStore(combineReducers(reducers));
sandbox.spy(store, "subscribe");
setSnippetEnabledPref(true);
({snippets, asrouterContent} = addSnippetsSubscriber(store));
sandbox.spy(asrouterContent, "init");
sandbox.spy(asrouterContent, "uninit");
// These need to be stubbed because they do dom stuff
sandbox.stub(asrouterContent, "_mount");
sandbox.stub(asrouterContent, "_unmount");
({snippets} = addSnippetsSubscriber(store));
sandbox.stub(snippets, "init").resolves();
sandbox.stub(snippets, "uninit");
@ -473,7 +466,6 @@ describe("addSnippetsSubscriber", () => {
delete global.gSnippetsMap;
});
it("should initialize feeds.snippets pref is true and SnippetsProvider if .initialize is true", () => {
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterOnboardingCohort", value: 0}});
store.dispatch({type: at.SNIPPETS_DATA, data: {}});
assert.calledOnce(snippets.init);
});
@ -507,37 +499,20 @@ describe("addSnippetsSubscriber", () => {
store.dispatch({type: at.PREF_CHANGED, data: {name: "disableSnippets", value: true}});
assert.calledOnce(snippets.uninit);
});
it("should not initialize snippets if asrouterExperimentEnabled pref is true", () => {
store.dispatch({type: "FOO"});
it("should not initialize snippets if asrouterExperimentEnabled pref and snippets message provider pref are true", () => {
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouter.messageProviders", value: JSON.stringify([{id: "snippets", enabled: true}])}});
store.dispatch({type: at.SNIPPETS_DATA, data: {}});
assert.calledOnce(store.subscribe);
assert.notCalled(snippets.init);
});
describe("asrouter", () => {
it("should initialize asrouter once if asrouterExperimentEnabled and snippets pref are both true", () => {
store.dispatch({type: "FOO"});
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
it("should only initialize snippets if asrouterExperimentEnabled pref and snippets message provider pref are both false", () => {
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: false}});
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouter.messageProviders", value: JSON.stringify([{id: "snippets", enabled: false}])}});
store.dispatch({type: at.SNIPPETS_DATA, data: {}});
assert.calledOnce(asrouterContent.init);
assert.isTrue(asrouterContent.initialized);
});
it("should uninitialize asrouter if asrouterExperimentEnabled pref is turned off and there are no onboarding experiments running", () => {
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterOnboardingCohort", value: 0}});
assert.isTrue(asrouterContent.initialized);
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: false}});
assert.calledOnce(asrouterContent.uninit);
assert.isFalse(asrouterContent.initialized);
});
it("should uninitialize asrouter if snippets pref is turned off", () => {
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
assert.isTrue(asrouterContent.initialized);
store.dispatch({type: at.PREF_CHANGED, data: {name: "feeds.snippets", value: false}});
assert.calledOnce(asrouterContent.uninit);
assert.isFalse(asrouterContent.initialized);
});
assert.calledOnce(store.subscribe);
assert.calledOnce(snippets.init);
});
});

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

@ -17,10 +17,7 @@ describe("Screenshots", () => {
fakeServices = {
wm: {
getEnumerator() {
return {
hasMoreElements: () => true,
getNext: () => {}
};
return Array(10);
}
}
};
@ -124,7 +121,7 @@ describe("Screenshots", () => {
describe("#_shouldGetScreenshots", () => {
beforeEach(() => {
let more = 2;
sandbox.stub(global.Services.wm, "getEnumerator").returns({getNext: () => {}, hasMoreElements() { return more--; }});
sandbox.stub(global.Services.wm, "getEnumerator").callsFake(() => Array(Math.max(more--, 0)));
});
it("should use private browsing utils to determine if a window is private", () => {
Screenshots._shouldGetScreenshots();

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

@ -486,6 +486,21 @@ describe("TelemetryFeed", () => {
assert.propertyVal(ping, "source", "SNIPPETS");
assert.propertyVal(ping, "event", "CLICK");
});
it("should drop the default client_id if includeClientID presents", async () => {
const data = {
action: "snippet_user_event",
source: "SNIPPETS",
event: "CLICK",
message_id: "snippets_message_01",
includeClientID: true
};
const action = ac.ASRouterUserEvent(data);
const ping = await instance.createASRouterEvent(action);
assert.isUndefined(ping.client_id);
assert.isUndefined(ping.includeClientID);
assert.propertyVal(ping, "impression_id", "n/a");
});
});
describe("#sendEvent", () => {
it("should call PingCentre", async () => {

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

@ -504,7 +504,11 @@ describe("Top Stories Feed", () => {
instance.store.getState = () => ({Sections: [{id: "topstories", rows: response.recommendations}], Prefs: {values: {showSponsored: true}}});
globals.set("Math", {random: () => 0.4});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
instance.dispatchSpocDone = () => {};
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledOnce(instance.store.dispatch);
let [action] = instance.store.dispatch.firstCall.args;
@ -518,11 +522,17 @@ describe("Top Stories Feed", () => {
assert.equal(action.data.rows[2].pinned, true);
// Second new tab shouldn't trigger a section update event (spocsPerNewTab === 0.5)
globals.set("Math", {random: () => 0.6});
globals.set("Math", {
random: () => 0.6,
min: Math.min
});
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledOnce(instance.store.dispatch);
globals.set("Math", {random: () => 0.3});
globals.set("Math", {
random: () => 0.3,
min: Math.min
});
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledTwice(instance.store.dispatch);
[action] = instance.store.dispatch.secondCall.args;
@ -536,6 +546,7 @@ describe("Top Stories Feed", () => {
});
it("should delay inserting spoc if stories haven't been fetched", async () => {
let fetchStub = globals.sandbox.stub();
instance.dispatchSpocDone = () => {};
sectionsManagerStub.sections.set("topstories", {
options: {
show_spocs: true,
@ -545,7 +556,10 @@ describe("Top Stories Feed", () => {
});
globals.set("fetch", fetchStub);
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
globals.set("Math", {random: () => 0.4});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
const response = {
"settings": {"spocsPerNewTabs": 0.5},
@ -569,6 +583,7 @@ describe("Top Stories Feed", () => {
});
it("should not insert spoc if preffed off", async () => {
let fetchStub = globals.sandbox.stub();
instance.dispatchSpocDone = () => {};
sectionsManagerStub.sections.set("topstories", {
options: {
show_spocs: false,
@ -594,8 +609,23 @@ describe("Top Stories Feed", () => {
assert.calledOnce(instance.shouldShowSpocs);
assert.notCalled(instance.store.dispatch);
});
it("should call dispatchSpocDone when calling maybeAddSpoc", async () => {
instance.dispatchSpocDone = sinon.spy();
instance.storiesLoaded = true;
await instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledOnce(instance.dispatchSpocDone);
assert.calledWith(instance.dispatchSpocDone, {});
});
it("should fire POCKET_WAITING_FOR_SPOC action with false", () => {
instance.dispatchSpocDone({});
assert.calledOnce(instance.store.dispatch);
const [action] = instance.store.dispatch.firstCall.args;
assert.equal(action.type, "POCKET_WAITING_FOR_SPOC");
assert.equal(action.data, false);
});
it("should not insert spoc if user opted out", async () => {
let fetchStub = globals.sandbox.stub();
instance.dispatchSpocDone = () => {};
sectionsManagerStub.sections.set("topstories", {
options: {
show_spocs: true,
@ -620,6 +650,7 @@ describe("Top Stories Feed", () => {
});
it("should not fail if there is no spoc", async () => {
let fetchStub = globals.sandbox.stub();
instance.dispatchSpocDone = () => {};
sectionsManagerStub.sections.set("topstories", {
options: {
show_spocs: true,
@ -629,7 +660,10 @@ describe("Top Stories Feed", () => {
});
globals.set("fetch", fetchStub);
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
globals.set("Math", {random: () => 0.4});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
const response = {
"settings": {"spocsPerNewTabs": 0.5},
@ -646,7 +680,10 @@ describe("Top Stories Feed", () => {
let fetchStub = globals.sandbox.stub();
globals.set("fetch", fetchStub);
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
globals.set("Math", {random: () => 0.4});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
const response = {
"settings": {"spocsPerNewTabs": 0.5},
@ -741,6 +778,7 @@ describe("Top Stories Feed", () => {
});
it("should maintain frequency caps when inserting spocs", async () => {
let fetchStub = globals.sandbox.stub();
instance.dispatchSpocDone = () => {};
sectionsManagerStub.sections.set("topstories", {
options: {
show_spocs: true,
@ -805,6 +843,7 @@ describe("Top Stories Feed", () => {
});
it("should maintain client-side MAX_LIFETIME_CAP", async () => {
let fetchStub = globals.sandbox.stub();
instance.dispatchSpocDone = () => {};
sectionsManagerStub.sections.set("topstories", {
options: {
show_spocs: true,

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

@ -180,7 +180,7 @@ const TEST_GLOBAL = {
createNullPrincipal() {},
getSystemPrincipal() {}
},
wm: {getMostRecentWindow: () => window, getEnumerator: () => ({hasMoreElements: () => false})},
wm: {getMostRecentWindow: () => window, getEnumerator: () => []},
ww: {registerNotification() {}, unregisterNotification() {}},
appinfo: {appBuildID: "20180710100040"}
},
@ -194,6 +194,7 @@ const TEST_GLOBAL = {
},
defineLazyGlobalGetters() {},
defineLazyModuleGetter() {},
defineLazyModuleGetters() {},
defineLazyServiceGetter() {},
generateQI() { return {}; }
},