Backed out 2 changesets (bug 1486931, bug 1486631) for browser_onboarding_accessibility.js failures CLOSED TREE

Backed out changeset 8a25fc40764a (bug 1486931)
Backed out changeset a89328c87888 (bug 1486631)
This commit is contained in:
Ciure Andrei 2018-08-29 01:55:19 +03:00
Родитель 0267a22bb2
Коммит 46180390ae
88 изменённых файлов: 644 добавлений и 1748 удалений

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

@ -63,7 +63,6 @@ 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,8 +37,7 @@ const INITIAL_STATE = {
visible: false,
data: {}
},
Sections: [],
Pocket: {waitingForSpoc: true}
Sections: []
};
function App(prevState = INITIAL_STATE.App, action) {
@ -382,19 +381,10 @@ 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, Pocket};
this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections};
const EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];

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

@ -1,9 +1,7 @@
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";
@ -11,7 +9,6 @@ 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();
@ -30,5 +27,4 @@ ReactDOM.hydrate(<Provider store={store}>
strings={global.gActivityStreamStrings} />
</Provider>, document.getElementById("root"));
enableASRouterContent(store, asrouterContent);
addSnippetsSubscriber(store);

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

@ -5,25 +5,7 @@
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"]`
`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"
}
]
```
`snippetsUrl` | The main remote endpoint that serves all snippet messages | `String` | `https://activity-stream-icons.services.mozilla.com/v1/messages.json.br`
## Admin Interface

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

@ -176,11 +176,6 @@ 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,6 +4,8 @@ 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)
@ -94,7 +96,6 @@ 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.
@ -169,11 +170,4 @@ 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"
}
```

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

@ -1,147 +0,0 @@
{
"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,13 +9,7 @@ class OnboardingCard extends React.PureComponent {
onClick() {
const {props} = this;
const ping = {
event: "CLICK_BUTTON",
message_id: props.id,
id: props.UISurface,
includeClientID: true
};
props.sendUserActionTelemetry(ping);
props.sendUserActionTelemetry({event: "CLICK_BUTTON", message_id: props.id, id: props.UISurface});
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.messageBlockList.includes(msg.id);
const impressions = this.state.messageImpressions[msg.id] ? this.state.impressions[msg.id].length : 0;
const isBlocked = this.state.blockList.includes(msg.id);
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
let itemClassName = "message-item";
if (isCurrent) { itemClassName += " current"; }
@ -84,18 +84,10 @@ export class ASRouterAdmin extends React.PureComponent {
renderProviders() {
return (<table><tbody>
{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>);
})}
{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>))}
</tbody></table>);
}

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

@ -114,7 +114,7 @@ export class _Base extends React.PureComponent {
const {initialized} = App;
const prefs = props.Prefs.values;
if (prefs.asrouterExperimentEnabled && window.location.hash === "#asrouter") {
if ((prefs.asrouterExperimentEnabled || prefs.asrouterOnboardingCohort > 0) && window.location.hash === "#asrouter") {
return (<ASRouterAdmin />);
}
@ -124,12 +124,7 @@ 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
let isOnboardingEnabled = false;
try {
isOnboardingEnabled = JSON.parse(prefs["asrouter.messageProviders"]).find(i => i.id === "onboarding").enabled;
} catch (e) {}
if (isOnboardingEnabled) {
if (prefs.asrouterOnboardingCohort > 0) {
global.document.body.classList.add("hide-onboarding");
}

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

@ -129,7 +129,6 @@ 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;
@ -153,13 +152,7 @@ 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" : "";
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 ? (
cards.push(link ? (
<Card key={i}
index={i}
className={className}
@ -225,7 +218,7 @@ Section.defaultProps = {
title: ""
};
export const SectionIntl = connect(state => ({Prefs: state.Prefs, Pocket: state.Pocket}))(injectIntl(Section));
export const SectionIntl = connect(state => ({Prefs: state.Prefs}))(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 fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}`);
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?entrypoint=
activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email`);
if (response.status === 200) {
const {flowId, flowBeginTime} = await response.json();
this.setState({flowId, flowBeginTime});

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

@ -8,7 +8,6 @@ $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
@ -137,7 +136,6 @@ $hover-transition-duration: 150ms;
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);
@ -204,24 +202,10 @@ $hover-transition-duration: 150ms;
background-image: url('#{$image-path}glyph-search-16.svg');
background-size: 26px;
background-color: $blue-60;
border-radius: $default-icon-wrapper-size;
border-radius: 42px;
-moz-context-properties: fill;
fill: $white;
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;
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow);
}
// We want all search shortcuts to have a white background in case they have transparency.
@ -592,6 +576,13 @@ $hover-transition-duration: 150ms;
}
}
// 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;
@ -601,6 +592,11 @@ $hover-transition-duration: 150ms;
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 {

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

@ -1,13 +0,0 @@
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,6 +7,7 @@ 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
@ -376,16 +377,13 @@ 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();
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;
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort > 0;
// state.Prefs.values["feeds.snippets"]: Should snippets be shown?
// state.Snippets.initialized Is the snippets data initialized?
// snippets.initialized: Is SnippetsProvider currently initialised?
@ -409,8 +407,22 @@ 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();
}
});
// Returned for testing purposes
return {snippets};
// These values are returned for testing purposes
return {snippets, asrouterContent};
}

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

@ -157,7 +157,7 @@ $textbox-shadow-size: 4px;
position: absolute;
top: -($context-menu-button-size / 2);
transform: scale(0.25);
transition-duration: 150ms;
transition-duration: 200ms;
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: 150ms;
transition-duration: 200ms;
transition-property: transform, opacity;
width: 27px; }
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
@ -524,8 +524,7 @@ main {
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase;
transition: box-shadow 150ms; }
text-transform: uppercase; }
.top-site-outer .tile::before {
content: attr(data-fallback); }
.top-site-outer .screenshot {
@ -577,16 +576,7 @@ main {
border-radius: 42px;
-moz-context-properties: fill;
fill: #FFF;
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; }
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF; }
.top-site-outer .title {
@ -832,12 +822,19 @@ 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 +1458,7 @@ a.firstrun-link {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 150ms;
transition-duration: 200ms;
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: 150ms;
transition-duration: 200ms;
transition-property: transform, opacity;
width: 27px; }
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
@ -527,8 +527,7 @@ main {
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase;
transition: box-shadow 150ms; }
text-transform: uppercase; }
.top-site-outer .tile::before {
content: attr(data-fallback); }
.top-site-outer .screenshot {
@ -580,16 +579,7 @@ main {
border-radius: 42px;
-moz-context-properties: fill;
fill: #FFF;
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; }
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF; }
.top-site-outer .title {
@ -835,12 +825,19 @@ 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); }
@ -1464,7 +1461,7 @@ a.firstrun-link {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 150ms;
transition-duration: 200ms;
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: 150ms;
transition-duration: 200ms;
transition-property: transform, opacity;
width: 27px; }
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
@ -524,8 +524,7 @@ main {
font-size: 32px;
font-weight: 200;
justify-content: center;
text-transform: uppercase;
transition: box-shadow 150ms; }
text-transform: uppercase; }
.top-site-outer .tile::before {
content: attr(data-fallback); }
.top-site-outer .screenshot {
@ -577,16 +576,7 @@ main {
border-radius: 42px;
-moz-context-properties: fill;
fill: #FFF;
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; }
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
.top-site-outer.search-shortcut .rich-icon {
background-color: #FFF; }
.top-site-outer .title {
@ -832,12 +822,19 @@ 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 +1458,7 @@ a.firstrun-link {
position: absolute;
top: -13.5px;
transform: scale(0.25);
transition-duration: 150ms;
transition-duration: 200ms;
transition-property: transform, opacity;
width: 27px; }
.card-outer .context-menu-button:-moz-any(:active, :focus) {

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

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

@ -92,18 +92,16 @@
__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_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);
/* 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);
@ -114,12 +112,9 @@ __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);
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();
new content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_3__["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,
@ -128,17 +123,16 @@ 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_9___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
react_redux__WEBPACK_IMPORTED_MODULE_7__["Provider"],
react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
react_redux__WEBPACK_IMPORTED_MODULE_5__["Provider"],
{ store: store },
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__["Base"], {
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_2__["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)))
@ -211,7 +205,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", "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"]) {
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"]) {
actionTypes[type] = type;
}
@ -489,6 +483,7 @@ __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";
@ -501,6 +496,7 @@ 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
@ -880,18 +876,13 @@ 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();
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;
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort > 0;
// state.Prefs.values["feeds.snippets"]: Should snippets be shown?
// state.Snippets.initialized Is the snippets data initialized?
// snippets.initialized: Is SnippetsProvider currently initialised?
@ -906,10 +897,18 @@ 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();
}
}));
// Returned for testing purposes
return { snippets };
// These values are returned for testing purposes
return { snippets, asrouterContent };
}
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
@ -923,18 +922,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__(41);
/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(40);
/* 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__(40);
/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(44);
/* 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 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__(42);
/* harmony import */ var _templates_SimpleSnippet_SimpleSnippet__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(41);
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; };
@ -1118,11 +1117,6 @@ 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: {} });
@ -1637,7 +1631,7 @@ class _Base extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
const { initialized } = App;
const prefs = props.Prefs.values;
if (prefs.asrouterExperimentEnabled && window.location.hash === "#asrouter") {
if ((prefs.asrouterExperimentEnabled || prefs.asrouterOnboardingCohort > 0) && window.location.hash === "#asrouter") {
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_2__["ASRouterAdmin"], null);
}
@ -1647,12 +1641,7 @@ 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
let isOnboardingEnabled = false;
try {
isOnboardingEnabled = JSON.parse(prefs["asrouter.messageProviders"]).find(i => i.id === "onboarding").enabled;
} catch (e) {}
if (isOnboardingEnabled) {
if (prefs.asrouterOnboardingCohort > 0) {
global.document.body.classList.add("hide-onboarding");
}
@ -1819,8 +1808,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.messageBlockList.includes(msg.id);
const impressions = this.state.messageImpressions[msg.id] ? this.state.impressions[msg.id].length : 0;
const isBlocked = this.state.blockList.includes(msg.id);
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
let itemClassName = "message-item";
if (isCurrent) {
@ -1896,32 +1885,24 @@ 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) => {
let label = "(local)";
if (provider.type === "remote") {
label = react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
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(
"a",
{ target: "_blank", href: provider.url },
provider.url
);
} 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
)
);
})
) : "(local)"
)
))
)
);
}
@ -2480,7 +2461,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__(45);
/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(44);
/* 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);
@ -2626,7 +2607,6 @@ 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;
@ -2649,13 +2629,7 @@ 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" : "";
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,
cards.push(link ? 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,
@ -2722,7 +2696,7 @@ Section.defaultProps = {
title: ""
};
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));
const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Prefs: state.Prefs }))(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(Section));
class _Sections extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
renderSections() {
@ -4022,8 +3996,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__(43);
/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(46);
/* 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 _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; };
@ -4431,7 +4405,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__(43);
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __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; };
@ -5003,8 +4977,8 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
if (_this.props.fxa_endpoint && !_this.didFetch) {
try {
_this.didFetch = true;
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?${fxaParams}`);
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?entrypoint=
activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email`);
if (response.status === 200) {
const { flowId, flowBeginTime } = yield response.json();
_this.setState({ flowId, flowBeginTime });
@ -5251,27 +5225,6 @@ 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
@ -7370,7 +7323,7 @@ function ftl(strings) {
/***/ }),
/* 41 */
/* 40 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -7383,7 +7336,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__(40);
var src = __webpack_require__(39);
// CONCATENATED MODULE: ./node_modules/fluent-react/src/localization.js
@ -7885,7 +7838,7 @@ localized_Localized.propTypes = {
/***/ }),
/* 42 */
/* 41 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8037,7 +7990,7 @@ class SimpleSnippet_SimpleSnippet extends external_React_default.a.PureComponent
}
/***/ }),
/* 43 */
/* 42 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8123,8 +8076,7 @@ const INITIAL_STATE = {
visible: false,
data: {}
},
Sections: [],
Pocket: { waitingForSpoc: true }
Sections: []
};
@ -8470,19 +8422,10 @@ function Snippets(prevState = INITIAL_STATE.Snippets, action) {
}
}
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 };
var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections };
/***/ }),
/* 44 */
/* 43 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8553,13 +8496,7 @@ class OnboardingMessage_OnboardingCard extends external_React_default.a.PureComp
onClick() {
const { props } = this;
const ping = {
event: "CLICK_BUTTON",
message_id: props.id,
id: props.UISurface,
includeClientID: true
};
props.sendUserActionTelemetry(ping);
props.sendUserActionTelemetry({ event: "CLICK_BUTTON", message_id: props.id, id: props.UISurface });
props.onAction(props.content.button_action);
}
@ -8626,7 +8563,7 @@ class OnboardingMessage_OnboardingMessage extends external_React_default.a.PureC
}
/***/ }),
/* 45 */
/* 44 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";
@ -8990,7 +8927,7 @@ const Card = Object(external_ReactRedux_["connect"])(state => ({ platform: state
const PlaceholderCard = props => external_React_default.a.createElement(Card, { placeholder: true, className: props.className });
/***/ }),
/* 46 */
/* 45 */
/***/ (function(module, __webpack_exports__, __webpack_require__) {
"use strict";

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

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

До

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

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

До

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

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

До

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

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

До

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

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

До

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

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

До

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

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

@ -576,11 +576,10 @@ 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",
"action": ["snippets_user_event" | "onboarding_user_event"],
"addon_version": "20180710100040",
"impression_id": "{005deed0-e3e4-4c02-a041-17405fd703f6}",
"locale": "en-US",
@ -590,20 +589,6 @@ 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,9 +142,7 @@ 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/CFRMessageProvider.jsm"),
path.resolve("lib/CFRPageActions.jsm")
path.resolve("lib/OnboardingMessageProvider.jsm")
]
}
]

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

@ -5,16 +5,11 @@
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");
@ -24,7 +19,6 @@ 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 = {
@ -32,10 +26,8 @@ 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, CFRMessageProvider};
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider};
const STARTPAGE_VERSION = "0.1.0";
const MessageLoaderUtils = {
@ -78,29 +70,6 @@ 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
*
@ -111,8 +80,6 @@ const MessageLoaderUtils = {
switch (provider.type) {
case "remote":
return this._remoteLoader;
case "remote-settings":
return this._remoteSettingsLoader;
case "local":
default:
return this._localLoader;
@ -177,10 +144,8 @@ class _ASRouter {
this._state = {
lastMessageId: null,
providers: [],
messageBlockList: [],
providerBlockList: [],
messageImpressions: {},
providerImpressions: {},
blockList: [],
impressions: {},
messages: []
};
this._triggerHandler = this._triggerHandler.bind(this);
@ -206,11 +171,7 @@ class _ASRouter {
const providers = existingPreviewProvider ? [existingPreviewProvider] : [];
const providersJSON = Services.prefs.getStringPref(this._messageProviderPref, "");
try {
JSON.parse(providersJSON).forEach(provider => {
if (provider.enabled) {
providers.push(provider);
}
});
JSON.parse(providersJSON).forEach(provider => providers.push(provider));
} catch (e) {
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
}
@ -322,13 +283,10 @@ class _ASRouter {
this._storage = storage;
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
this.dispatchToAS = dispatchToAS;
this.dispatch = this.dispatch.bind(this);
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});
const blockList = await this._storage.get("blockList") || [];
const impressions = await this._storage.get("impressions") || {};
await this.setState({blockList, impressions});
this._updateMessageProviders();
await this.loadMessagesFromAllProviders();
@ -378,57 +336,18 @@ class _ASRouter {
}
}
_findMessage(candidateMessages, trigger) {
const messages = candidateMessages.filter(m => this.isBelowFrequencyCaps(m));
_findMessage(messages, target, trigger) {
const {impressions} = this.state;
// 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, trigger, onError: this._handleTargetingError});
return ASRouterTargeting.findMatchingMessage({messages, impressions, 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}];
@ -448,7 +367,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, trigger);
const message = await this._findMessage(bundledMessagesOfSameTemplate, target, trigger);
if (!message) {
/* istanbul ignore next */ // Code coverage in mochitests
break;
@ -474,65 +393,40 @@ class _ASRouter {
_getUnblockedMessages() {
let {state} = this;
return state.messages.filter(item =>
!state.messageBlockList.includes(item.id) &&
!state.providerBlockList.includes(item.provider)
);
return state.messages.filter(item => !state.blockList.includes(item.id));
}
async _sendMessageToTarget(message, target, trigger, force = false) {
// 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") {
if (force) {
CFRPageActions.forceRecommendation(target, message, this.dispatch);
} else {
CFRPageActions.addRecommendation(target, trigger.param, message, this.dispatch);
}
// New tab single messages
} else {
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});
} else {
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
}
}
async addImpression(message) {
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};
});
// Don't store impressions for messages that don't include any limits on frequency
if (!message.frequency) {
return;
}
}
// 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;
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};
});
}
/**
@ -553,51 +447,41 @@ 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 both message impressions and provider impressions are
* cleared is as follows (where `item` is either `message` or `provider`):
* but the current behaviour for when impressions are cleared is as follows:
*
* 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
* 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
* than the longest time period will be cleared.
*/
async cleanupImpressions() {
await this.setState(state => {
const messageImpressions = this._cleanupImpressionsForItems(state, state.messages, "messageImpressions");
const providerImpressions = this._cleanupImpressionsForItems(state, state.providers, "providerImpressions");
return {messageImpressions, providerImpressions};
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};
});
}
// 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;
@ -606,7 +490,7 @@ class _ASRouter {
if (previewMsgs.length) {
[message] = previewMsgs;
} else {
message = await this._findMessage(msgs, trigger);
message = await this._findMessage(msgs, target, trigger);
}
if (previewMsgs.length) {
@ -625,33 +509,33 @@ class _ASRouter {
await this.setState({lastMessageId: id});
const newMessage = this.getMessageById(id);
await this._sendMessageToTarget(newMessage, target, action.data, force);
await this._sendMessageToTarget(newMessage, target, force, action.data);
}
async blockMessageById(idOrIds) {
async blockById(idOrIds) {
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
await this.setState(state => {
const messageBlockList = [...state.messageBlockList, ...idsToBlock];
const blockList = [...state.blockList, ...idsToBlock];
// When a message is blocked, its impressions should be cleared as well
const messageImpressions = {...state.messageImpressions};
idsToBlock.forEach(id => delete messageImpressions[id]);
this._storage.set("messageBlockList", messageBlockList);
return {messageBlockList, messageImpressions};
const impressions = {...state.impressions};
idsToBlock.forEach(id => delete impressions[id]);
this._storage.set("blockList", blockList);
return {blockList, impressions};
});
}
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};
});
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);
}
}
_validPreviewEndpoint(url) {
@ -695,7 +579,7 @@ class _ASRouter {
// To be passed to ASRouterTriggerListeners
async _triggerHandler(target, trigger) {
await this.onMessage({target, data: {type: "TRIGGER", data: {trigger}}});
await this.onMessage({target, data: {type: "TRIGGER", trigger}});
}
_removePreviewEndpoint(state) {
@ -718,13 +602,19 @@ class _ASRouter {
target.browser.ownerGlobal.OpenBrowserWindow({private: true});
break;
case ra.OPEN_URL:
target.browser.ownerGlobal.openLinkIn(action.data.url, "tabshifted", {
private: false,
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})
this.openLinkIn(action.data.url, target, {
isPrivate: false,
where: "tabshifted",
triggeringPrincipal: Services.scriptSecurityManager.getNullPrincipal({})
});
break;
case ra.OPEN_ABOUT_PAGE:
target.browser.ownerGlobal.openTrustedLinkIn(`about:${action.data.page}`, "tab");
this.openLinkIn(`about:${action.data.page}`, target, {
isPrivate: false,
trusted: true,
where: "tab",
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
});
break;
case ra.OPEN_APPLICATIONS_MENU:
UITour.showMenu(target.browser.ownerGlobal, action.data.target);
@ -735,10 +625,6 @@ class _ASRouter {
}
}
dispatch(action, target) {
this.onMessage({data: action, target});
}
async onMessage({data: action, target}) {
switch (action.type) {
case "USER_ACTION":
@ -759,41 +645,29 @@ class _ASRouter {
await this.sendNextMessage(target, (action.data && action.data.trigger) || {});
break;
case "BLOCK_MESSAGE_BY_ID":
await this.blockMessageById(action.data.id);
await this.blockById(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.blockMessageById(action.data.bundle.map(b => b.id));
await this.blockById(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 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};
const blockList = [...state.blockList];
blockList.splice(blockList.indexOf(action.data.id), 1);
this._storage.set("blockList", blockList);
return {blockList};
});
break;
case "UNBLOCK_BUNDLE":
await this.setState(state => {
const messageBlockList = [...state.messageBlockList];
const blockList = [...state.blockList];
for (let message of action.data.bundle) {
messageBlockList.splice(messageBlockList.indexOf(message.id), 1);
blockList.splice(blockList.indexOf(message.id), 1);
}
this._storage.set("messageBlockList", messageBlockList);
return {messageBlockList};
this._storage.set("blockList", blockList);
return {blockList};
});
break;
case "OVERRIDE_MESSAGE":
@ -808,7 +682,7 @@ class _ASRouter {
}
break;
case "IMPRESSION":
await this.addImpression(action.data);
this.addImpression(action.data);
break;
}
}

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

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

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

@ -13,9 +13,13 @@ ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
"resource://gre/modules/TelemetryEnvironment.jsm");
const FXA_USERNAME_PREF = "services.sync.username";
const ONBOARDING_MESSAGE_PROVDIER_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const ONBOARDING_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouterOnboardingCohort";
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
@ -67,9 +71,6 @@ const TargetingGetters = {
update: settings.update
};
},
get currentDate() {
return new Date();
},
get profileAgeCreated() {
return new ProfileAge(null, null).created;
},
@ -108,6 +109,7 @@ 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
@ -126,15 +128,18 @@ 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 => (
{
@ -145,16 +150,10 @@ const TargetingGetters = {
}
)));
},
// Temporary targeting function for the purposes of running the simplified onboarding experience
get isInExperimentCohort() {
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;
return Services.prefs.getIntPref(ONBOARDING_EXPERIMENT_PREF, 0);
}
};
@ -179,6 +178,35 @@ 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
*
@ -216,7 +244,7 @@ this.ASRouterTargeting = {
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
* @returns {obj} an AS router message
*/
async findMatchingMessage({messages, trigger, context, onError}) {
async findMatchingMessage({messages, impressions = {}, trigger, context, onError}) {
const arrayOfItems = [...messages];
let match;
let candidate;
@ -227,6 +255,7 @@ 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, {id: "openURL", param: host});
this._triggerHandler(aBrowser.messageManager, {id: "openURL", param: host});
}
} catch (e) {} // Couldn't parse location URL
}

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

@ -192,6 +192,10 @@ 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",
@ -199,26 +203,16 @@ 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",
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release",
cohort: 0
localProvider: "OnboardingMessageProvider"
}, {
id: "snippets",
type: "remote",
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
url: "https://activity-stream-icons.services.mozilla.com/v1/messages.json.br",
updateCycleInMs: ONE_HOUR_IN_MS * 4
}])
}]
]);

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

@ -1,286 +0,0 @@
/* 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,14 +34,10 @@ 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 = [];
@ -78,13 +74,6 @@ 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
@ -155,7 +144,7 @@ class PageAction {
const mainAction = {
label: primary.label,
accessKey: primary.accessKey,
callback: () => this.dispatchUserAction(primary.action)
callback: () => this._dispatchToASRouter(primary.action)
};
const secondaryActions = [{
@ -220,36 +209,18 @@ const CFRPageActions = {
}
},
/**
* Force a recommendation to be shown. Should only happen via the Admin page.
* @param browser The browser for the recommendation
* @param recommendation The recommendation to show
* @param dispatchToASRouter A function to dispatch resulting actions to
* @return Did adding the recommendation succeed?
*/
async forceRecommendation(browser, recommendation, dispatchToASRouter) {
// If we are forcing via the Admin page, the browser comes in a different format
const win = browser.browser.ownerGlobal;
const {id, content} = recommendation;
RecommendationMap.set(browser.browser, {id, content});
if (!PageActionMap.has(win)) {
PageActionMap.set(win, new PageAction(win, dispatchToASRouter));
}
await PageActionMap.get(win).show(recommendation.content.notification_text, true);
return true;
},
/**
* Add a recommendation specific to the given browser and host.
* @param browser The browser for the recommendation
* @param host The host for the recommendation
* @param recommendation The recommendation to show
* @param dispatchToASRouter A function to dispatch resulting actions to
* @param force Force the recommendation to appear if the host doesn't match
* @return Did adding the recommendation succeed?
*/
async addRecommendation(browser, host, recommendation, dispatchToASRouter) {
async addRecommendation(browser, host, recommendation, dispatchToASRouter, force = false) {
const win = browser.ownerGlobal;
if (browser !== win.gBrowser.selectedBrowser || !isHostMatch(browser, host)) {
if (browser !== win.gBrowser.selectedBrowser || !(force || isHostMatch(browser, host))) {
return false;
}
const {id, content} = recommendation;
@ -272,6 +243,5 @@ const CFRPageActions = {
RecommendationMap.clear();
}
};
this.CFRPageActions = CFRPageActions;
const EXPORTED_SYMBOLS = ["CFRPageActions"];

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

@ -364,12 +364,6 @@ 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",
@ -377,12 +371,6 @@ 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,25 +311,17 @@ 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;
}
@ -339,7 +331,6 @@ this.TopStoriesFeed = class TopStoriesFeed {
if (!spocs.length) {
// There's currently no spoc left to display
this.dispatchSpocDone(target);
return false;
}
@ -351,7 +342,6 @@ 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,7 +173,6 @@ 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=تنويه الخصوصية
@ -202,3 +201,4 @@ firstrun_privacy_notice=تنويه الخصوصية
firstrun_continue_to_login=تابِع
firstrun_skip_login=تجاوز هذه الخطوة
section_menu_action_add_search_engine=أضِف محرك بحث

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

@ -173,7 +173,6 @@ 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=Политика за личните данни
@ -202,3 +201,4 @@ firstrun_privacy_notice=Политиката за лични данни
firstrun_continue_to_login=Продължаване
firstrun_skip_login=Пропускане
section_menu_action_add_search_engine=Добавяне на търсеща машина

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

@ -173,7 +173,6 @@ 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=গোপনীয়তা নীতি
@ -181,24 +180,19 @@ 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=Elimina de l'historial
menu_action_remove_download=Suprimeix de l'historial
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
@ -173,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -192,8 +191,6 @@ 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.
@ -202,3 +199,4 @@ 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,7 +173,6 @@ 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=ખાનગી સૂચના
@ -202,3 +201,4 @@ firstrun_privacy_notice=ખાનગી સૂચના
firstrun_continue_to_login=ચાલુ રાખો
firstrun_skip_login=આ પગલું છોડી દો
section_menu_action_add_search_engine=શોધ યંત્ર ઉમેરો

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

@ -173,7 +173,6 @@ 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=गोपनीयता नीति
@ -202,3 +201,4 @@ firstrun_privacy_notice=गोपनीयता नीति
firstrun_continue_to_login=जारी रखें
firstrun_skip_login=इस चरण को छोड़ दें
section_menu_action_add_search_engine=सर्च इंजन जोड़े

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

@ -173,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,7 +173,6 @@ 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=개인 정보 보호 정책
@ -202,3 +201,4 @@ firstrun_privacy_notice=개인 정보 보호 정책
firstrun_continue_to_login=계속
firstrun_skip_login=단계 건너뛰기
section_menu_action_add_search_engine=검색 엔진 추가

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

@ -173,7 +173,6 @@ 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
@ -202,3 +201,4 @@ 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,9 +50,6 @@ 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):
@ -98,12 +95,9 @@ 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=शीर्ष साइट्स
@ -126,16 +120,13 @@ 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.
@ -162,18 +153,12 @@ 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=गोपनीयता सूचना
@ -181,24 +166,21 @@ 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,7 +172,6 @@ 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
@ -201,3 +200,4 @@ 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,7 +99,6 @@ 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
@ -116,3 +115,4 @@ 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,7 +173,6 @@ 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=Уведомление о приватности
@ -202,3 +201,4 @@ firstrun_privacy_notice=политикой приватности
firstrun_continue_to_login=Продолжить
firstrun_skip_login=Пропустить этот шаг
section_menu_action_add_search_engine=Добавить поисковую систему

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

@ -192,7 +192,6 @@ 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,7 +173,6 @@ 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=ประกาศความเป็นส่วนตัว
@ -192,7 +191,6 @@ 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=เงื่อนไขการให้บริการ
@ -200,3 +198,4 @@ 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": "আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।",
"firstrun_learn_more_link": "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_form_header": "আপনার ই-মেইল লিখুন",
"firstrun_form_sub_header": "Firefox সিঙ্ক চালিয়ে যেতে",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "ইমেইল",
"firstrun_invalid_input": "কার্যকর ইমেইল আবশ্যক",
"firstrun_extra_legal_links": "অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {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": "আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।",
"firstrun_learn_more_link": "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_form_header": "আপনার ই-মেইল লিখুন",
"firstrun_form_sub_header": "Firefox সিঙ্ক চালিয়ে যেতে",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "ইমেইল",
"firstrun_invalid_input": "কার্যকর ইমেইল আবশ্যক",
"firstrun_extra_legal_links": "অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {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": "Elimina de l'historial",
"menu_action_remove_download": "Suprimeix 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": "Baliozko helbide elektronikoa behar da",
"firstrun_invalid_input": "Valid email required",
"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": "समाविष्ट करणारे फोल्डर उघडा",
"menu_action_show_file_linux": "समाविष्ट करणारे फोल्डर उघडा",
"menu_action_show_file_default": "फाईल दाखवा",
"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_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": "प्रायोजित कथा",
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
"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_highlights_options_download_label": "Most Recent Download",
"prefs_highlights_options_pocket_label": "Pages Saved to 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": "सानुकूल प्रतिमा URL",
"topsites_form_image_url_label": "Custom Image URL",
"topsites_form_url_placeholder": "URL चिकटवा किंवा टाईप करा",
"topsites_form_use_image_link": "सानुकूल प्रतिमा वापरा…",
"topsites_form_use_image_link": "Use a custom image…",
"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": "Image failed to load. Try a different 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": "अरेरे, हा मजकूर लोड करताना काहीतरी गोंधळ झाला.",
"error_fallback_default_refresh_suggestion": "पुन्हा प्रयत्न करण्यासाठी पृष्ठ रिफ्रेश करा.",
"error_fallback_default_info": "Oops, something went wrong loading this content.",
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
"section_menu_action_remove_section": "विभाग काढा",
"section_menu_action_collapse_section": "विभाग ढासळा",
"section_menu_action_expand_section": "विभाग वाढवा",
"section_menu_action_manage_section": "विभाग व्यवस्थापित करा",
"section_menu_action_expand_section": "Expand Section",
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_manage_webext": "एक्सटेन्शन व्यवस्थापित करा",
"section_menu_action_add_topsite": "खास साईट्स जोडा",
"section_menu_action_add_topsite": "Add Top Site",
"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": "आपले बुकमार्क्स, इतिहास, पासवर्ड आणि इतर सेटिंग आपल्या सर्व उपकरणांवर मिळवा.",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Firefox खात्यांविषयी अधिक जाणून घ्या",
"firstrun_form_header": "ईमेल प्रविष्ट करा",
"firstrun_form_sub_header": "Firefox Sync वर सुरू ठेवण्यासाठी",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "ईमेल",
"firstrun_invalid_input": "वैध ईमेल आवश्यक",
"firstrun_extra_legal_links": "पुढे जाताना आपण {terms} आणि {privacy} यांना संमती देता.",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {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": "Zahtevan je veljaven e-poštni naslov",
"firstrun_invalid_input": "Valid email required",
"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,8 +56,5 @@ window.gActivityStreamPrerenderedState = {
"order": 2,
"initialized": false
}
],
"Pocket": {
"waitingForSpoc": true
}
]
};

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

@ -110,16 +110,6 @@ 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,
@ -301,12 +291,3 @@ 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);
});

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

@ -1,36 +0,0 @@
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,17 +6,15 @@ 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, FAKE_REMOTE_SETTINGS_PROVIDER];
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_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;
@ -35,10 +33,8 @@ describe("ASRouter", () => {
let Router;
let channel;
let sandbox;
let messageBlockList;
let providerBlockList;
let messageImpressions;
let providerImpressions;
let blockList;
let impressions;
let fetchStub;
let clock;
let getStringPrefStub;
@ -47,10 +43,8 @@ describe("ASRouter", () => {
function createFakeStorage() {
const getStub = sandbox.stub();
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));
getStub.withArgs("blockList").returns(Promise.resolve(blockList));
getStub.withArgs("impressions").returns(Promise.resolve(impressions));
return {
get: getStub,
set: sandbox.stub().returns(Promise.resolve())
@ -72,10 +66,8 @@ describe("ASRouter", () => {
}
beforeEach(async () => {
messageBlockList = [];
providerBlockList = [];
messageImpressions = {};
providerImpressions = {};
blockList = [];
impressions = {};
sandbox = sinon.sandbox.create();
clock = sandbox.useFakeTimers();
fetchStub = sandbox.stub(global, "fetch")
@ -108,23 +100,23 @@ describe("ASRouter", () => {
assert.calledOnce(addObserverStub);
assert.calledWith(addObserverStub, MESSAGE_PROVIDER_PREF_NAME);
});
it("should set state.messageBlockList to the block list in persistent storage", async () => {
messageBlockList = ["foo"];
it("should set state.blockList to the block list in persistent storage", async () => {
blockList = ["foo"];
Router = new _ASRouter({providers: FAKE_PROVIDERS});
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.deepEqual(Router.state.messageBlockList, ["foo"]);
assert.deepEqual(Router.state.blockList, ["foo"]);
});
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,
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,
// otherwise they will be cleaned up by .cleanupImpressions()
const testMessage = {id: "foo", frequency: {lifetimeCap: 10}};
messageImpressions = {foo: [0, 1, 2]};
impressions = {foo: [0, 1, 2]};
Router = new _ASRouter({providers: [{id: "onboarding", type: "local", messages: [testMessage]}]});
await Router.init(channel, createFakeStorage(), dispatchStub);
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
assert.deepEqual(Router.state.impressions, impressions);
});
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
Router = new _ASRouter(MESSAGE_PROVIDER_PREF_NAME, FAKE_LOCAL_PROVIDERS);
@ -145,7 +137,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, FAKE_REMOTE_SETTINGS_PROVIDER]);
setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider]);
const {length} = Router.state.providers;
await Router.observe("", "", MESSAGE_PROVIDER_PREF_NAME);
@ -185,7 +177,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", enabled: true, url: "http://fake.com/endpoint", updateCycleInMs: 300}
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300}
]);
const previousState = Router.state;
@ -197,7 +189,7 @@ describe("ASRouter", () => {
});
it("should not trigger an update if we only have local providers", async () => {
await createRouterAndInit([
{id: "foo", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES}
{id: "foo", type: "local", messages: FAKE_LOCAL_MESSAGES}
]);
const previousState = Router.state;
@ -210,8 +202,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", enabled: true, updateCycleInMs: 300},
{id: "alocalprovider", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES}
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300},
{id: "alocalprovider", type: "local", messages: FAKE_LOCAL_MESSAGES}
]);
fetchStub
.withArgs("http://fake.com/endpoint")
@ -230,7 +222,7 @@ describe("ASRouter", () => {
/* eslint-disable object-curly-newline */ /* eslint-disable object-property-newline */
await createRouterAndInit([
{id: "foo", type: "local", enabled: true, messages: [
{id: "foo", type: "local", 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"}}
@ -244,10 +236,6 @@ 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", () => {
@ -255,7 +243,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", enabled: true, type: "remote", url: "https://www.mozilla.org/%STARTPAGE_VERSION%/"};
const provider = {id: "foo", 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}/`);
@ -266,29 +254,19 @@ describe("ASRouter", () => {
const stub = sandbox.stub(global.Services.urlFormatter, "formatURL")
.withArgs(url)
.returns(replacedUrl);
const provider = {id: "foo", enabled: true, type: "remote", url};
const provider = {id: "foo", 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(() => ({messageBlockList: ALL_MESSAGE_IDS.slice(1)}));
await Router.setState(() => ({blockList: ALL_MESSAGE_IDS.slice(1)}));
const targetStub = {sendAsyncMessage: sandbox.stub()};
await Router.sendNextMessage(targetStub);
@ -296,19 +274,8 @@ 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(() => ({messageBlockList: ALL_MESSAGE_IDS}));
await Router.setState(() => ({blockList: ALL_MESSAGE_IDS}));
const targetStub = {sendAsyncMessage: sandbox.stub()};
await Router.sendNextMessage(targetStub);
@ -433,84 +400,59 @@ describe("ASRouter", () => {
});
describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
it("should add the id to the blockList 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.messageBlockList.includes("foo"));
assert.isTrue(Router.state.blockList.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 messageBlockList and send a CLEAR_BUNDLE message", async () => {
it("should add all the ids in the bundle to the blockList 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.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
assert.calledWithExactly(Router._storage.set, "blockList", bundleIds);
});
});
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
it("should remove the id from the messageBlockList", async () => {
it("should remove the id from the blockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isTrue(Router.state.messageBlockList.includes("foo"));
assert.isTrue(Router.state.blockList.includes("foo"));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
assert.isFalse(Router.state.messageBlockList.includes("foo"));
assert.isFalse(Router.state.blockList.includes("foo"));
});
it("should save the messageBlockList", async () => {
it("should save the blockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
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", []);
assert.calledWithExactly(Router._storage.set, "blockList", []);
});
});
describe("#onMessage: UNBLOCK_BUNDLE", () => {
it("should remove all the ids in the bundle from the messageBlockList", async () => {
it("should remove all the ids in the bundle from the blockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
assert.isFalse(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
assert.isFalse(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
});
it("should save the messageBlockList", async () => {
it("should save the blockList", async () => {
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
assert.calledWithExactly(Router._storage.set, "blockList", []);
});
});
@ -606,15 +548,15 @@ describe("ASRouter", () => {
await Router.onMessage(msg);
assert.calledOnce(Router._findMessage);
assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
assert.deepEqual(Router._findMessage.firstCall.args[2], {id: "firstRun"});
});
it("consider the trigger when picking a message", async () => {
const messages = [
let messages = [
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}}
];
const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
const message = await Router._findMessage(messages, data.data.trigger);
const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
let message = await Router._findMessage(messages, target, {id: "foo"});
assert.equal(message, messages[0]);
});
it("should pick a message with the right targeting and trigger", async () => {
@ -641,26 +583,6 @@ describe("ASRouter", () => {
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
});
it("should call CFRPageActions.forceRecommendation if the template is cfr_action and force is true", async () => {
sandbox.stub(CFRPageActions, "forceRecommendation");
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.forceRecommendation);
});
it("should call CFRPageActions.addRecommendation if the template is cfr_action and force is false", async () => {
sandbox.stub(CFRPageActions, "addRecommendation");
const testMessage = {id: "foo", template: "cfr_doorhanger"};
await Router.setState({messages: [testMessage]});
await Router._sendMessageToTarget(testMessage, {}, {}, false);
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);
@ -678,23 +600,24 @@ 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");
});
});
@ -710,21 +633,6 @@ 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");
@ -732,7 +640,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", data: {trigger}}});
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", trigger}});
});
});
@ -763,181 +671,26 @@ describe("ASRouter", () => {
});
describe("impressions", () => {
async function addProviderWithFrequency(id, frequency) {
await Router.setState(state => {
const newProvider = {id, frequency};
const providers = [...state.providers, newProvider];
return {providers};
});
}
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);
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);
});
assert.isArray(Router.state.impressions.foo);
assert.deepEqual(Router.state.impressions.foo, [42]);
assert.calledWith(Router._storage.set, "impressions", {foo: [42]});
});
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);
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);
});
assert.notProperty(Router.state.impressions, "foo");
assert.notCalled(Router._storage.set);
});
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", () => {
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);
@ -955,59 +708,58 @@ describe("ASRouter", () => {
assert.isNull(Router.getLongestPeriod(message));
});
});
describe("cleanup on init", () => {
it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
it("should clear impressions for messages which do not exist in state.messages", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}];
messageImpressions = {foo: [0], bar: [0, 1]};
impressions = {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, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
it("should clear impressions 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}]}}];
messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
impressions = {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, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
it("should clear impressions 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}]}}];
messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
impressions = {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, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should clear messageImpressions if they are not properly formatted", async () => {
it("should clear impressions if they are not properly formatted", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}];
// this is impromperly formatted since messageImpressions are supposed to be an array
messageImpressions = {foo: 0};
// this is impromperly formatted since impressions are supposed to be an array
impressions = {foo: 0};
const result = {};
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
assert.calledWith(Router._storage.set, "messageImpressions", result);
assert.deepEqual(Router.state.messageImpressions, result);
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.calledWith(Router._storage.set, "impressions", result);
assert.deepEqual(Router.state.impressions, result);
});
it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
it("should not clear impressions for messages which do exist in state.messages", async () => {
const messages = [{id: "foo", frequency: {lifetime: 10}}, {id: "bar", frequency: {lifetime: 10}}];
messageImpressions = {foo: [0], bar: []};
impressions = {foo: [0], bar: []};
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
assert.notCalled(Router._storage.set);
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
assert.deepEqual(Router.state.impressions, impressions);
});
});
});

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

@ -1,27 +1,100 @@
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#isInExperimentCohort", () => {
let sandbox;
beforeEach(() => {
sandbox = sinon.sandbox.create();
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);
});
});
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);
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]));
});
});
});

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

@ -13,7 +13,11 @@ describe("ASRouterTriggerListeners", () => {
function resetEnumeratorStub(windows) {
windowEnumeratorStub
.withArgs("navigator:browser")
.returns(windows);
.returns({
_count: -1,
hasMoreElements() { this._count++; return this._count < windows.length; },
getNext() { return windows[this._count]; }
});
}
beforeEach(async () => {
@ -117,12 +121,12 @@ describe("ASRouterTriggerListeners", () => {
const newTriggerHandler = sinon.stub();
openURLListener.init(newTriggerHandler, hosts);
const browser = {};
const browser = {messageManager: {}};
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, {id: "openURL", param: "www.mozilla.org"});
assert.calledWithExactly(newTriggerHandler, browser.messageManager, {id: "openURL", param: "www.mozilla.org"});
});
});
});

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

@ -9,15 +9,13 @@ 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", enabled: true, cohort: 0};
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", localProvider: "FAKE_LOCAL_PROVIDER"};
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", enabled: true};
export const FAKE_REMOTE_SETTINGS_PROVIDER = {id: "remotey-settingsy", type: "remote-settings", bucket: "bucketname", enabled: true};
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint"};
// Stubs methods on RemotePageManager
export class FakeRemotePageManager {

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

@ -1,41 +0,0 @@
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, Pocket} = reducers;
const {TopSites, App, Snippets, Prefs, Dialog, Sections} = reducers;
import {actionTypes as at} from "common/Actions.jsm";
describe("Reducers", () => {
@ -601,13 +601,4 @@ 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,11 +15,6 @@ 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;
@ -181,33 +176,21 @@ describe("<Section>", () => {
};
});
it("should not render for empty topics", () => {
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
wrapper = mountSectionWithProps(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 = mountSectionIntlWithProps(TOP_STORIES_SECTION);
wrapper = mountSectionWithProps(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 = mountSectionIntlWithProps(TOP_STORIES_SECTION);
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
assert.lengthOf(wrapper.find(".topic"), 1);
});

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

@ -445,6 +445,7 @@ describe("addSnippetsSubscriber", () => {
let store;
let sandbox;
let snippets;
let asrouterContent;
function setSnippetEnabledPref(value) {
store.dispatch({type: at.PREF_CHANGED, data: {name: "feeds.snippets", value}});
}
@ -453,7 +454,13 @@ describe("addSnippetsSubscriber", () => {
store = createStore(combineReducers(reducers));
sandbox.spy(store, "subscribe");
setSnippetEnabledPref(true);
({snippets} = addSnippetsSubscriber(store));
({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");
sandbox.stub(snippets, "init").resolves();
sandbox.stub(snippets, "uninit");
@ -466,6 +473,7 @@ 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);
});
@ -499,20 +507,37 @@ 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 and snippets message provider pref are true", () => {
it("should not initialize snippets if asrouterExperimentEnabled pref is true", () => {
store.dispatch({type: "FOO"});
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);
});
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: {}});
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}});
assert.calledOnce(store.subscribe);
assert.calledOnce(snippets.init);
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);
});
});
});

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

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

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

@ -486,21 +486,6 @@ 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,11 +504,7 @@ describe("Top Stories Feed", () => {
instance.store.getState = () => ({Sections: [{id: "topstories", rows: response.recommendations}], Prefs: {values: {showSponsored: true}}});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
instance.dispatchSpocDone = () => {};
globals.set("Math", {random: () => 0.4});
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledOnce(instance.store.dispatch);
let [action] = instance.store.dispatch.firstCall.args;
@ -522,17 +518,11 @@ 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,
min: Math.min
});
globals.set("Math", {random: () => 0.6});
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledOnce(instance.store.dispatch);
globals.set("Math", {
random: () => 0.3,
min: Math.min
});
globals.set("Math", {random: () => 0.3});
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
assert.calledTwice(instance.store.dispatch);
[action] = instance.store.dispatch.secondCall.args;
@ -546,7 +536,6 @@ 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,
@ -556,10 +545,7 @@ describe("Top Stories Feed", () => {
});
globals.set("fetch", fetchStub);
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
globals.set("Math", {random: () => 0.4});
const response = {
"settings": {"spocsPerNewTabs": 0.5},
@ -583,7 +569,6 @@ 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,
@ -609,23 +594,8 @@ 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,
@ -650,7 +620,6 @@ 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,
@ -660,10 +629,7 @@ describe("Top Stories Feed", () => {
});
globals.set("fetch", fetchStub);
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
globals.set("Math", {
random: () => 0.4,
min: Math.min
});
globals.set("Math", {random: () => 0.4});
const response = {
"settings": {"spocsPerNewTabs": 0.5},
@ -680,10 +646,7 @@ 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,
min: Math.min
});
globals.set("Math", {random: () => 0.4});
const response = {
"settings": {"spocsPerNewTabs": 0.5},
@ -778,7 +741,6 @@ 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,
@ -843,7 +805,6 @@ 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: () => []},
wm: {getMostRecentWindow: () => window, getEnumerator: () => ({hasMoreElements: () => false})},
ww: {registerNotification() {}, unregisterNotification() {}},
appinfo: {appBuildID: "20180710100040"}
},
@ -194,7 +194,6 @@ const TEST_GLOBAL = {
},
defineLazyGlobalGetters() {},
defineLazyModuleGetter() {},
defineLazyModuleGetters() {},
defineLazyServiceGetter() {},
generateQI() { return {}; }
},