mirror of
https://github.com/mozilla/gecko-dev.git
synced 2024-10-08 19:04:45 +00:00
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:
parent
0267a22bb2
commit
46180390ae
@ -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) {
|
||||
|
File diff suppressed because one or more lines are too long
@ -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) {
|
||||
|
File diff suppressed because one or more lines are too long
@ -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) {
|
||||
|
File diff suppressed because one or more lines are too long
@ -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";
|
||||
|
File diff suppressed because one or more lines are too long
Binary file not shown.
Before Width: | Height: | Size: 1.6 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 3.3 KiB |
Binary file not shown.
Before Width: | Height: | Size: 1.7 KiB |
Binary file not shown.
Before Width: | Height: | Size: 6.4 KiB |
Binary file not shown.
Before Width: | Height: | Size: 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 l’extension
|
||||
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 {}; }
|
||||
},
|
||||
|
Loading…
Reference in New Issue
Block a user