зеркало из https://github.com/mozilla/gecko-dev.git
Bug 1486631 - Add CFR, search shortcut fixes, and bug fixes to Activity Stream r=Mardak,ursula
MozReview-Commit-ID: HZbYyg4FGwi Differential Revision: https://phabricator.services.mozilla.com/D4392 --HG-- extra : moz-landing-system : lando
This commit is contained in:
Родитель
8741a0d774
Коммит
d21e43c53a
|
@ -63,6 +63,7 @@ for (const type of [
|
|||
"PLACES_LINK_BLOCKED",
|
||||
"PLACES_LINK_DELETED",
|
||||
"PLACES_SAVED_TO_POCKET",
|
||||
"POCKET_WAITING_FOR_SPOC",
|
||||
"PREFS_INITIAL_VALUES",
|
||||
"PREF_CHANGED",
|
||||
"PREVIEW_REQUEST",
|
||||
|
|
|
@ -37,7 +37,8 @@ const INITIAL_STATE = {
|
|||
visible: false,
|
||||
data: {}
|
||||
},
|
||||
Sections: []
|
||||
Sections: [],
|
||||
Pocket: {waitingForSpoc: true}
|
||||
};
|
||||
|
||||
function App(prevState = INITIAL_STATE.App, action) {
|
||||
|
@ -381,10 +382,19 @@ function Snippets(prevState = INITIAL_STATE.Snippets, action) {
|
|||
}
|
||||
}
|
||||
|
||||
function Pocket(prevState = INITIAL_STATE.Pocket, action) {
|
||||
switch (action.type) {
|
||||
case at.POCKET_WAITING_FOR_SPOC:
|
||||
return {...prevState, waitingForSpoc: action.data};
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
this.INITIAL_STATE = INITIAL_STATE;
|
||||
this.TOP_SITES_DEFAULT_ROWS = TOP_SITES_DEFAULT_ROWS;
|
||||
this.TOP_SITES_MAX_SITES_PER_ROW = TOP_SITES_MAX_SITES_PER_ROW;
|
||||
|
||||
this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections};
|
||||
this.reducers = {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket};
|
||||
|
||||
const EXPORTED_SYMBOLS = ["reducers", "INITIAL_STATE", "insertPinned", "TOP_SITES_DEFAULT_ROWS", "TOP_SITES_MAX_SITES_PER_ROW"];
|
||||
|
|
|
@ -1,7 +1,9 @@
|
|||
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
|
||||
import {addSnippetsSubscriber} from "content-src/lib/snippets";
|
||||
import {ASRouterContent} from "content-src/asrouter/asrouter-content";
|
||||
import {Base} from "content-src/components/Base/Base";
|
||||
import {DetectUserSessionStart} from "content-src/lib/detect-user-session-start";
|
||||
import {enableASRouterContent} from "content-src/lib/asroutercontent";
|
||||
import {initStore} from "content-src/lib/init-store";
|
||||
import {Provider} from "react-redux";
|
||||
import React from "react";
|
||||
|
@ -9,6 +11,7 @@ import ReactDOM from "react-dom";
|
|||
import {reducers} from "common/Reducers.jsm";
|
||||
|
||||
const store = initStore(reducers, global.gActivityStreamPrerenderedState);
|
||||
const asrouterContent = new ASRouterContent();
|
||||
|
||||
new DetectUserSessionStart(store).sendEventOrAddListener();
|
||||
|
||||
|
@ -27,4 +30,5 @@ ReactDOM.hydrate(<Provider store={store}>
|
|||
strings={global.gActivityStreamStrings} />
|
||||
</Provider>, document.getElementById("root"));
|
||||
|
||||
enableASRouterContent(store, asrouterContent);
|
||||
addSnippetsSubscriber(store);
|
||||
|
|
|
@ -5,7 +5,25 @@
|
|||
Name | Used for | Type | Example value
|
||||
--- | --- | --- | ---
|
||||
`whitelistHosts` | Whitelist a host in order to fetch messages from its endpoint | `[String]` | `["gist.github.com", "gist.githubusercontent.com", "localhost:8000"]`
|
||||
`snippetsUrl` | The main remote endpoint that serves all snippet messages | `String` | `https://activity-stream-icons.services.mozilla.com/v1/messages.json.br`
|
||||
`messageProviders` | Message provider options | `Object` | [see below](#message-providers)
|
||||
|
||||
### Message providers
|
||||
|
||||
```json
|
||||
[
|
||||
{
|
||||
"id" : "onboarding",
|
||||
"type" : "local",
|
||||
"localProvider" : "OnboardingMessageProvider"
|
||||
},
|
||||
{
|
||||
"type" : "remote",
|
||||
"url" : "https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json",
|
||||
"updateCycleInMs" : 14400000,
|
||||
"id" : "snippets"
|
||||
}
|
||||
]
|
||||
```
|
||||
|
||||
## Admin Interface
|
||||
|
||||
|
|
|
@ -176,6 +176,11 @@ export class ASRouterUISurface extends React.PureComponent {
|
|||
this.setState({message: {}});
|
||||
}
|
||||
break;
|
||||
case "CLEAR_PROVIDER":
|
||||
if (action.data.id === this.state.message.provider) {
|
||||
this.setState({message: {}});
|
||||
}
|
||||
break;
|
||||
case "CLEAR_BUNDLE":
|
||||
if (this.state.bundle.bundle) {
|
||||
this.setState({bundle: {}});
|
||||
|
|
|
@ -4,8 +4,6 @@ Field name | Type | Required | Description | Example / Note
|
|||
--- | --- | --- | --- | ---
|
||||
`id` | `string` | Yes | A unique identifier for the message that should not conflict with any other previous message | `ONBOARDING_1`
|
||||
`template` | `string` | Yes | An id matching an existing Activity Stream Router template | [See example](https://github.com/mozilla/activity-stream/blob/33669c67c2269078a6d3d6d324fb48175d98f634/system-addon/content-src/message-center/templates/SimpleSnippet.jsx)
|
||||
`publish_start` | `date` | No | When to start showing the message | `1524474850876`
|
||||
`publish_end` | `date` | No | When to stop showing the message | `1524474850876`
|
||||
`content` | `object` | Yes | An object containing all variables/props to be rendered in the template. Subset of allowed tags detailed below. | [See example below](#html-subset)
|
||||
`bundled` | `integer` | No | The number of messages of the same template this one should be shown with | [See example below](#a-bundled-message-example)
|
||||
`order` | `integer` | No | If bundled with other messages of the same template, which order should this one be placed in? Defaults to 0 if no order is desired | [See example below](#a-bundled-message-example)
|
||||
|
@ -96,6 +94,7 @@ Name | Type | Example value | Description
|
|||
`isDefaultBrowser` | `Boolean` or `null` | Is Firefox the user's default browser? If we could not determine the default browser, this value is `null`
|
||||
`profileAgeCreated` | Number | `1522843725924` | Profile creation timestamp
|
||||
`profileAgeReset` | `Number` or `undefined` | `1522843725924` | When (if) the profile was reset
|
||||
`currentDate` | `Date` | `Date 2018-08-22T15:48:04.100Z` | Date object of current time in UTC
|
||||
`searchEngines` | `Object` | [example below](#searchengines-example) | Information about the current and available search engines
|
||||
`browserSettings.attribution` | `Object` or `undefined` | [example below](#attribution-example) | Attribution for the source of of where the browser was downloaded.
|
||||
|
||||
|
@ -170,4 +169,11 @@ Examples:
|
|||
// targeting addon information
|
||||
"targeting": "addonsInfo.addons['activity-stream@mozilla.org'].name == 'Activity Stream'"
|
||||
}
|
||||
|
||||
{
|
||||
"id": "7866",
|
||||
"content": {...},
|
||||
// targeting based on time
|
||||
"targeting": "currentDate > '2018-08-08'|date"
|
||||
}
|
||||
```
|
||||
|
|
|
@ -0,0 +1,147 @@
|
|||
{
|
||||
"title": "ExtensionDoorhanger",
|
||||
"description": "A template with a heading, addon icon, title and description. No markup allowed.",
|
||||
"version": "1.0.0",
|
||||
"type": "object",
|
||||
"definitions": {
|
||||
"plainText": {
|
||||
"description": "Plain text (no HTML allowed)",
|
||||
"type": "string"
|
||||
},
|
||||
"linkUrl": {
|
||||
"description": "Target for links or buttons",
|
||||
"type": "string",
|
||||
"format": "uri"
|
||||
}
|
||||
},
|
||||
"properties": {
|
||||
"notification_text": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Text for location bar chiclet."}
|
||||
]
|
||||
},
|
||||
"info_icon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Text for button tooltip used to provider information about the doorhanger."}
|
||||
]
|
||||
},
|
||||
"sumo_path": {
|
||||
"type": "string",
|
||||
"description": "Last part of the path in the URL to the support page with the information about the doorhanger.",
|
||||
"examples": ["extensionpromotions", "extensionrecommendations"]
|
||||
}
|
||||
}
|
||||
},
|
||||
"heading_text": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Doorhanger heading describing its purpose."}
|
||||
]
|
||||
},
|
||||
"addon": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"name": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Addon name"}
|
||||
]
|
||||
},
|
||||
"author": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Addon author"}
|
||||
]
|
||||
},
|
||||
"icon": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/linkUrl"},
|
||||
{"description": "Addon icon"}
|
||||
]
|
||||
},
|
||||
"amo_url": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/linkUrl"},
|
||||
{"description": "Link that offers more information related to the addon."}
|
||||
]
|
||||
}
|
||||
}
|
||||
},
|
||||
"text": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Description for the addon."}
|
||||
]
|
||||
},
|
||||
"buttons": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"primary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Text for the primary button of the doorhanger."}
|
||||
]
|
||||
},
|
||||
"accessKey": {
|
||||
"type": "string",
|
||||
"description": "A single character to be used as a shortcut key for the primary button. This should be one of the characters that appears in the button label."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Action dispatched by the button."
|
||||
},
|
||||
"data": {
|
||||
"properties": {
|
||||
"url": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/linkUrl"},
|
||||
{"description": "URL used in combination with the primary action dispatched."}
|
||||
]
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"secondary": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"label": {
|
||||
"allOf": [
|
||||
{"$ref": "#/definitions/plainText"},
|
||||
{"description": "Text for the secondary button of the doorhanger."}
|
||||
]
|
||||
},
|
||||
"accessKey": {
|
||||
"type": "string",
|
||||
"description": "A single character to be used as a shortcut key for the secondary button. This should be one of the characters that appears in the button label."
|
||||
},
|
||||
"action": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"type": {
|
||||
"type": "string",
|
||||
"description": "Action dispatched by the button."
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"additionalProperties": false,
|
||||
"required": ["notification_text", "heading_text", "addon", "text", "buttons"]
|
||||
}
|
|
@ -9,7 +9,13 @@ class OnboardingCard extends React.PureComponent {
|
|||
|
||||
onClick() {
|
||||
const {props} = this;
|
||||
props.sendUserActionTelemetry({event: "CLICK_BUTTON", message_id: props.id, id: props.UISurface});
|
||||
const ping = {
|
||||
event: "CLICK_BUTTON",
|
||||
message_id: props.id,
|
||||
id: props.UISurface,
|
||||
includeClientID: true
|
||||
};
|
||||
props.sendUserActionTelemetry(ping);
|
||||
props.onAction(props.content.button_action);
|
||||
}
|
||||
|
||||
|
|
|
@ -53,8 +53,8 @@ export class ASRouterAdmin extends React.PureComponent {
|
|||
|
||||
renderMessageItem(msg) {
|
||||
const isCurrent = msg.id === this.state.lastMessageId;
|
||||
const isBlocked = this.state.blockList.includes(msg.id);
|
||||
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
|
||||
const isBlocked = this.state.messageBlockList.includes(msg.id);
|
||||
const impressions = this.state.messageImpressions[msg.id] ? this.state.impressions[msg.id].length : 0;
|
||||
|
||||
let itemClassName = "message-item";
|
||||
if (isCurrent) { itemClassName += " current"; }
|
||||
|
@ -84,10 +84,18 @@ export class ASRouterAdmin extends React.PureComponent {
|
|||
|
||||
renderProviders() {
|
||||
return (<table><tbody>
|
||||
{this.state.providers.map((provider, i) => (<tr className="message-item" key={i}>
|
||||
{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>{provider.type === "remote" ? <a target="_blank" href={provider.url}>{provider.url}</a> : "(local)"}</td>
|
||||
</tr>))}
|
||||
<td>{label}</td>
|
||||
</tr>);
|
||||
})}
|
||||
</tbody></table>);
|
||||
}
|
||||
|
||||
|
|
|
@ -114,7 +114,7 @@ export class _Base extends React.PureComponent {
|
|||
const {initialized} = App;
|
||||
|
||||
const prefs = props.Prefs.values;
|
||||
if ((prefs.asrouterExperimentEnabled || prefs.asrouterOnboardingCohort > 0) && window.location.hash === "#asrouter") {
|
||||
if (prefs.asrouterExperimentEnabled && window.location.hash === "#asrouter") {
|
||||
return (<ASRouterAdmin />);
|
||||
}
|
||||
|
||||
|
@ -124,7 +124,12 @@ export class _Base extends React.PureComponent {
|
|||
|
||||
// Until we can delete the existing onboarding tour, just hide the onboarding button when users are in
|
||||
// the new simplified onboarding experiment. CSS hacks ftw
|
||||
if (prefs.asrouterOnboardingCohort > 0) {
|
||||
let isOnboardingEnabled = false;
|
||||
try {
|
||||
isOnboardingEnabled = JSON.parse(prefs["asrouter.messageProviders"]).find(i => i.id === "onboarding").enabled;
|
||||
} catch (e) {}
|
||||
|
||||
if (isOnboardingEnabled) {
|
||||
global.document.body.classList.add("hide-onboarding");
|
||||
}
|
||||
|
||||
|
|
|
@ -129,6 +129,7 @@ export class Section extends React.PureComponent {
|
|||
pref, privacyNoticeURL, isFirst, isLast
|
||||
} = this.props;
|
||||
|
||||
const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
|
||||
const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
|
||||
const {numRows} = this;
|
||||
const maxCards = maxCardsPerRow * numRows;
|
||||
|
@ -152,7 +153,13 @@ export class Section extends React.PureComponent {
|
|||
// On narrow viewports, we only show 3 cards per row. We'll mark the rest as
|
||||
// .hide-for-narrow to hide in CSS via @media query.
|
||||
const className = (i >= maxCardsOnNarrow) ? "hide-for-narrow" : "";
|
||||
cards.push(link ? (
|
||||
let usePlaceholder = !link;
|
||||
// If we are in the third card and waiting for spoc,
|
||||
// use the placeholder.
|
||||
if (!usePlaceholder && i === 2 && waitingForSpoc) {
|
||||
usePlaceholder = true;
|
||||
}
|
||||
cards.push(!usePlaceholder ? (
|
||||
<Card key={i}
|
||||
index={i}
|
||||
className={className}
|
||||
|
@ -218,7 +225,7 @@ Section.defaultProps = {
|
|||
title: ""
|
||||
};
|
||||
|
||||
export const SectionIntl = connect(state => ({Prefs: state.Prefs}))(injectIntl(Section));
|
||||
export const SectionIntl = connect(state => ({Prefs: state.Prefs, Pocket: state.Pocket}))(injectIntl(Section));
|
||||
|
||||
export class _Sections extends React.PureComponent {
|
||||
renderSections() {
|
||||
|
|
|
@ -26,8 +26,8 @@ export class _StartupOverlay extends React.PureComponent {
|
|||
if (this.props.fxa_endpoint && !this.didFetch) {
|
||||
try {
|
||||
this.didFetch = true;
|
||||
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?entrypoint=
|
||||
activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email`);
|
||||
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
|
||||
const response = await fetch(`${this.props.fxa_endpoint}/metrics-flow?${fxaParams}`);
|
||||
if (response.status === 200) {
|
||||
const {flowId, flowBeginTime} = await response.json();
|
||||
this.setState({flowId, flowBeginTime});
|
||||
|
|
|
@ -8,6 +8,7 @@ $default-icon-wrapper-size: 42px;
|
|||
$default-icon-size: 32px;
|
||||
$default-icon-offset: 6px;
|
||||
$half-base-gutter: $base-gutter / 2;
|
||||
$hover-transition-duration: 150ms;
|
||||
|
||||
.top-sites {
|
||||
// Take back the margin from the bottom row of vertical spacing as well as the
|
||||
|
@ -136,6 +137,7 @@ $half-base-gutter: $base-gutter / 2;
|
|||
font-weight: 200;
|
||||
justify-content: center;
|
||||
text-transform: uppercase; // sass-lint:disable-line no-disallowed-properties
|
||||
transition: box-shadow $hover-transition-duration;
|
||||
|
||||
&::before {
|
||||
content: attr(data-fallback);
|
||||
|
@ -202,10 +204,24 @@ $half-base-gutter: $base-gutter / 2;
|
|||
background-image: url('#{$image-path}glyph-search-16.svg');
|
||||
background-size: 26px;
|
||||
background-color: $blue-60;
|
||||
border-radius: 42px;
|
||||
border-radius: $default-icon-wrapper-size;
|
||||
-moz-context-properties: fill;
|
||||
fill: $white;
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow);
|
||||
box-shadow: var(--newtab-card-shadow);
|
||||
transition-duration: $hover-transition-duration;
|
||||
transition-property: background-size, bottom, inset-inline-end, height, width;
|
||||
}
|
||||
|
||||
&:hover .search-topsite {
|
||||
$hover-icon-wrapper-size: $default-icon-wrapper-size + 4;
|
||||
$hover-icon-offset: -$default-icon-offset - 3;
|
||||
|
||||
background-size: 28px;
|
||||
border-radius: $hover-icon-wrapper-size;
|
||||
bottom: $hover-icon-offset;
|
||||
height: $hover-icon-wrapper-size;
|
||||
inset-inline-end: $hover-icon-offset;
|
||||
width: $hover-icon-wrapper-size;
|
||||
}
|
||||
|
||||
// We want all search shortcuts to have a white background in case they have transparency.
|
||||
|
@ -576,13 +592,6 @@ $half-base-gutter: $base-gutter / 2;
|
|||
}
|
||||
}
|
||||
|
||||
// when unselected, higlight the tile on hover
|
||||
[type='checkbox']:not(:checked) + label {
|
||||
.tile:hover {
|
||||
@include fade-in;
|
||||
}
|
||||
}
|
||||
|
||||
// checkmark changes
|
||||
[type='checkbox']:not(:checked) + label::after {
|
||||
opacity: 0;
|
||||
|
@ -592,11 +601,6 @@ $half-base-gutter: $base-gutter / 2;
|
|||
opacity: 1;
|
||||
}
|
||||
|
||||
// hover
|
||||
[type='checkbox'] + label:hover::before {
|
||||
border: 1px solid var(--newtab-link-primary-color);
|
||||
}
|
||||
|
||||
// accessibility
|
||||
[type='checkbox']:checked:focus + label::before,
|
||||
[type='checkbox']:not(:checked):focus + label::before {
|
||||
|
|
|
@ -0,0 +1,13 @@
|
|||
export function enableASRouterContent(store, asrouterContent) {
|
||||
// Enable asrouter content
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
if (state.Prefs.values.asrouterExperimentEnabled && !asrouterContent.initialized) {
|
||||
asrouterContent.init();
|
||||
} else if (!state.Prefs.values.asrouterExperimentEnabled && asrouterContent.initialized) {
|
||||
asrouterContent.uninit();
|
||||
}
|
||||
});
|
||||
// Return this for testing purposes
|
||||
return {asrouterContent};
|
||||
}
|
|
@ -7,7 +7,6 @@ const SNIPPETS_ENABLED_EVENT = "Snippets:Enabled";
|
|||
const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
|
||||
|
||||
import {actionCreators as ac, actionTypes as at} from "common/Actions.jsm";
|
||||
import {ASRouterContent} from "content-src/asrouter/asrouter-content";
|
||||
|
||||
/**
|
||||
* SnippetsMap - A utility for cacheing values related to the snippet. It has
|
||||
|
@ -377,13 +376,16 @@ export class SnippetsProvider {
|
|||
*/
|
||||
export function addSnippetsSubscriber(store) {
|
||||
const snippets = new SnippetsProvider(store.dispatch);
|
||||
const asrouterContent = new ASRouterContent();
|
||||
|
||||
let initializing = false;
|
||||
|
||||
store.subscribe(async () => {
|
||||
const state = store.getState();
|
||||
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort > 0;
|
||||
let snippetsEnabled = false;
|
||||
try {
|
||||
snippetsEnabled = JSON.parse(state.Prefs.values["asrouter.messageProviders"]).find(i => i.id === "snippets").enabled;
|
||||
} catch (e) {}
|
||||
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && snippetsEnabled;
|
||||
// state.Prefs.values["feeds.snippets"]: Should snippets be shown?
|
||||
// state.Snippets.initialized Is the snippets data initialized?
|
||||
// snippets.initialized: Is SnippetsProvider currently initialised?
|
||||
|
@ -407,22 +409,8 @@ export function addSnippetsSubscriber(store) {
|
|||
) {
|
||||
snippets.uninit();
|
||||
}
|
||||
|
||||
// Turn on AS Router snippets if the experiment is enabled and the snippets pref is on;
|
||||
// otherwise, turn it off.
|
||||
if (
|
||||
(state.Prefs.values.asrouterExperimentEnabled || state.Prefs.values.asrouterOnboardingCohort > 0) &&
|
||||
state.Prefs.values["feeds.snippets"] &&
|
||||
!asrouterContent.initialized) {
|
||||
asrouterContent.init();
|
||||
} else if (
|
||||
((!state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort === 0) || !state.Prefs.values["feeds.snippets"]) &&
|
||||
asrouterContent.initialized
|
||||
) {
|
||||
asrouterContent.uninit();
|
||||
}
|
||||
});
|
||||
|
||||
// These values are returned for testing purposes
|
||||
return {snippets, asrouterContent};
|
||||
// Returned for testing purposes
|
||||
return {snippets};
|
||||
}
|
||||
|
|
|
@ -157,7 +157,7 @@ $textbox-shadow-size: 4px;
|
|||
position: absolute;
|
||||
top: -($context-menu-button-size / 2);
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: $context-menu-button-size;
|
||||
|
||||
|
|
|
@ -505,7 +505,7 @@ main {
|
|||
position: absolute;
|
||||
top: -13.5px;
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: 27px; }
|
||||
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
|
||||
|
@ -524,7 +524,8 @@ main {
|
|||
font-size: 32px;
|
||||
font-weight: 200;
|
||||
justify-content: center;
|
||||
text-transform: uppercase; }
|
||||
text-transform: uppercase;
|
||||
transition: box-shadow 150ms; }
|
||||
.top-site-outer .tile::before {
|
||||
content: attr(data-fallback); }
|
||||
.top-site-outer .screenshot {
|
||||
|
@ -576,7 +577,16 @@ main {
|
|||
border-radius: 42px;
|
||||
-moz-context-properties: fill;
|
||||
fill: #FFF;
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
|
||||
box-shadow: var(--newtab-card-shadow);
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-size, bottom, inset-inline-end, height, width; }
|
||||
.top-site-outer:hover .search-topsite {
|
||||
background-size: 28px;
|
||||
border-radius: 46px;
|
||||
bottom: -9px;
|
||||
height: 46px;
|
||||
inset-inline-end: -9px;
|
||||
width: 46px; }
|
||||
.top-site-outer.search-shortcut .rich-icon {
|
||||
background-color: #FFF; }
|
||||
.top-site-outer .title {
|
||||
|
@ -822,19 +832,12 @@ main {
|
|||
.topsite-form [type='checkbox']:checked + label .tile {
|
||||
box-shadow: 0 0 0 2px var(--newtab-link-primary-color); }
|
||||
|
||||
.topsite-form [type='checkbox']:not(:checked) + label .tile:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), 0 0 0 5px var(--newtab-card-active-outline-color);
|
||||
transition: box-shadow 150ms; }
|
||||
|
||||
.topsite-form [type='checkbox']:not(:checked) + label::after {
|
||||
opacity: 0; }
|
||||
|
||||
.topsite-form [type='checkbox']:checked + label::after {
|
||||
opacity: 1; }
|
||||
|
||||
.topsite-form [type='checkbox'] + label:hover::before {
|
||||
border: 1px solid var(--newtab-link-primary-color); }
|
||||
|
||||
.topsite-form [type='checkbox']:checked:focus + label::before,
|
||||
.topsite-form [type='checkbox']:not(:checked):focus + label::before {
|
||||
border: 1px dotted var(--newtab-link-primary-color); }
|
||||
|
@ -1458,7 +1461,7 @@ a.firstrun-link {
|
|||
position: absolute;
|
||||
top: -13.5px;
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: 27px; }
|
||||
.card-outer .context-menu-button:-moz-any(:active, :focus) {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -508,7 +508,7 @@ main {
|
|||
position: absolute;
|
||||
top: -13.5px;
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: 27px; }
|
||||
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
|
||||
|
@ -527,7 +527,8 @@ main {
|
|||
font-size: 32px;
|
||||
font-weight: 200;
|
||||
justify-content: center;
|
||||
text-transform: uppercase; }
|
||||
text-transform: uppercase;
|
||||
transition: box-shadow 150ms; }
|
||||
.top-site-outer .tile::before {
|
||||
content: attr(data-fallback); }
|
||||
.top-site-outer .screenshot {
|
||||
|
@ -579,7 +580,16 @@ main {
|
|||
border-radius: 42px;
|
||||
-moz-context-properties: fill;
|
||||
fill: #FFF;
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
|
||||
box-shadow: var(--newtab-card-shadow);
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-size, bottom, inset-inline-end, height, width; }
|
||||
.top-site-outer:hover .search-topsite {
|
||||
background-size: 28px;
|
||||
border-radius: 46px;
|
||||
bottom: -9px;
|
||||
height: 46px;
|
||||
inset-inline-end: -9px;
|
||||
width: 46px; }
|
||||
.top-site-outer.search-shortcut .rich-icon {
|
||||
background-color: #FFF; }
|
||||
.top-site-outer .title {
|
||||
|
@ -825,19 +835,12 @@ main {
|
|||
.topsite-form [type='checkbox']:checked + label .tile {
|
||||
box-shadow: 0 0 0 2px var(--newtab-link-primary-color); }
|
||||
|
||||
.topsite-form [type='checkbox']:not(:checked) + label .tile:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), 0 0 0 5px var(--newtab-card-active-outline-color);
|
||||
transition: box-shadow 150ms; }
|
||||
|
||||
.topsite-form [type='checkbox']:not(:checked) + label::after {
|
||||
opacity: 0; }
|
||||
|
||||
.topsite-form [type='checkbox']:checked + label::after {
|
||||
opacity: 1; }
|
||||
|
||||
.topsite-form [type='checkbox'] + label:hover::before {
|
||||
border: 1px solid var(--newtab-link-primary-color); }
|
||||
|
||||
.topsite-form [type='checkbox']:checked:focus + label::before,
|
||||
.topsite-form [type='checkbox']:not(:checked):focus + label::before {
|
||||
border: 1px dotted var(--newtab-link-primary-color); }
|
||||
|
@ -1461,7 +1464,7 @@ a.firstrun-link {
|
|||
position: absolute;
|
||||
top: -13.5px;
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: 27px; }
|
||||
.card-outer .context-menu-button:-moz-any(:active, :focus) {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -505,7 +505,7 @@ main {
|
|||
position: absolute;
|
||||
top: -13.5px;
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: 27px; }
|
||||
.top-site-outer .context-menu-button:-moz-any(:active, :focus) {
|
||||
|
@ -524,7 +524,8 @@ main {
|
|||
font-size: 32px;
|
||||
font-weight: 200;
|
||||
justify-content: center;
|
||||
text-transform: uppercase; }
|
||||
text-transform: uppercase;
|
||||
transition: box-shadow 150ms; }
|
||||
.top-site-outer .tile::before {
|
||||
content: attr(data-fallback); }
|
||||
.top-site-outer .screenshot {
|
||||
|
@ -576,7 +577,16 @@ main {
|
|||
border-radius: 42px;
|
||||
-moz-context-properties: fill;
|
||||
fill: #FFF;
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), var(--newtab-card-shadow); }
|
||||
box-shadow: var(--newtab-card-shadow);
|
||||
transition-duration: 150ms;
|
||||
transition-property: background-size, bottom, inset-inline-end, height, width; }
|
||||
.top-site-outer:hover .search-topsite {
|
||||
background-size: 28px;
|
||||
border-radius: 46px;
|
||||
bottom: -9px;
|
||||
height: 46px;
|
||||
inset-inline-end: -9px;
|
||||
width: 46px; }
|
||||
.top-site-outer.search-shortcut .rich-icon {
|
||||
background-color: #FFF; }
|
||||
.top-site-outer .title {
|
||||
|
@ -822,19 +832,12 @@ main {
|
|||
.topsite-form [type='checkbox']:checked + label .tile {
|
||||
box-shadow: 0 0 0 2px var(--newtab-link-primary-color); }
|
||||
|
||||
.topsite-form [type='checkbox']:not(:checked) + label .tile:hover {
|
||||
box-shadow: inset 0 0 0 1px var(--newtab-inner-box-shadow-color), 0 0 0 5px var(--newtab-card-active-outline-color);
|
||||
transition: box-shadow 150ms; }
|
||||
|
||||
.topsite-form [type='checkbox']:not(:checked) + label::after {
|
||||
opacity: 0; }
|
||||
|
||||
.topsite-form [type='checkbox']:checked + label::after {
|
||||
opacity: 1; }
|
||||
|
||||
.topsite-form [type='checkbox'] + label:hover::before {
|
||||
border: 1px solid var(--newtab-link-primary-color); }
|
||||
|
||||
.topsite-form [type='checkbox']:checked:focus + label::before,
|
||||
.topsite-form [type='checkbox']:not(:checked):focus + label::before {
|
||||
border: 1px dotted var(--newtab-link-primary-color); }
|
||||
|
@ -1458,7 +1461,7 @@ a.firstrun-link {
|
|||
position: absolute;
|
||||
top: -13.5px;
|
||||
transform: scale(0.25);
|
||||
transition-duration: 200ms;
|
||||
transition-duration: 150ms;
|
||||
transition-property: transform, opacity;
|
||||
width: 27px; }
|
||||
.card-outer .context-menu-button:-moz-any(:active, :focus) {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -92,16 +92,18 @@
|
|||
__webpack_require__.r(__webpack_exports__);
|
||||
/* WEBPACK VAR INJECTION */(function(global) {/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
||||
/* harmony import */ var content_src_lib_snippets__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(3);
|
||||
/* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(12);
|
||||
/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(38);
|
||||
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(7);
|
||||
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(16);
|
||||
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_5___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_5__);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
|
||||
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(10);
|
||||
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
|
||||
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(42);
|
||||
/* harmony import */ var content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(4);
|
||||
/* harmony import */ var content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(12);
|
||||
/* harmony import */ var content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(38);
|
||||
/* harmony import */ var content_src_lib_asroutercontent__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(39);
|
||||
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(7);
|
||||
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(16);
|
||||
/* harmony import */ var react_redux__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_redux__WEBPACK_IMPORTED_MODULE_7__);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(5);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_8___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_8__);
|
||||
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(10);
|
||||
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_9___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_9__);
|
||||
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(43);
|
||||
|
||||
|
||||
|
||||
|
@ -112,9 +114,12 @@ __webpack_require__.r(__webpack_exports__);
|
|||
|
||||
|
||||
|
||||
const store = Object(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_4__["initStore"])(common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__["reducers"], global.gActivityStreamPrerenderedState);
|
||||
|
||||
new content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_3__["DetectUserSessionStart"](store).sendEventOrAddListener();
|
||||
|
||||
const store = Object(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_6__["initStore"])(common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_10__["reducers"], global.gActivityStreamPrerenderedState);
|
||||
const asrouterContent = new content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_2__["ASRouterContent"]();
|
||||
|
||||
new content_src_lib_detect_user_session_start__WEBPACK_IMPORTED_MODULE_4__["DetectUserSessionStart"](store).sendEventOrAddListener();
|
||||
|
||||
// If we are starting in a prerendered state, we must wait until the first render
|
||||
// to request state rehydration (see Base.jsx). If we are NOT in a prerendered state,
|
||||
|
@ -123,16 +128,17 @@ if (!global.gActivityStreamPrerenderedState) {
|
|||
store.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].NEW_TAB_STATE_REQUEST }));
|
||||
}
|
||||
|
||||
react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
|
||||
react_redux__WEBPACK_IMPORTED_MODULE_5__["Provider"],
|
||||
react_dom__WEBPACK_IMPORTED_MODULE_9___default.a.hydrate(react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(
|
||||
react_redux__WEBPACK_IMPORTED_MODULE_7__["Provider"],
|
||||
{ store: store },
|
||||
react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_2__["Base"], {
|
||||
react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_Base_Base__WEBPACK_IMPORTED_MODULE_3__["Base"], {
|
||||
isFirstrun: global.document.location.href === "about:welcome",
|
||||
isPrerendered: !!global.gActivityStreamPrerenderedState,
|
||||
locale: global.document.documentElement.lang,
|
||||
strings: global.gActivityStreamStrings })
|
||||
), document.getElementById("root"));
|
||||
|
||||
Object(content_src_lib_asroutercontent__WEBPACK_IMPORTED_MODULE_5__["enableASRouterContent"])(store, asrouterContent);
|
||||
Object(content_src_lib_snippets__WEBPACK_IMPORTED_MODULE_1__["addSnippetsSubscriber"])(store);
|
||||
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
||||
|
||||
|
@ -205,7 +211,7 @@ const globalImportContext = typeof Window === "undefined" ? BACKGROUND_PROCESS :
|
|||
// }
|
||||
const actionTypes = {};
|
||||
|
||||
for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_ONBOARDING", "DOWNLOAD_CHANGED", "FILL_SEARCH_TERM", "INIT", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
|
||||
for (const type of ["ADDONS_INFO_REQUEST", "ADDONS_INFO_RESPONSE", "ARCHIVE_FROM_POCKET", "AS_ROUTER_TELEMETRY_USER_EVENT", "BLOCK_URL", "BOOKMARK_URL", "COPY_DOWNLOAD_LINK", "DELETE_BOOKMARK_BY_ID", "DELETE_FROM_POCKET", "DELETE_HISTORY_URL", "DIALOG_CANCEL", "DIALOG_OPEN", "DISABLE_ONBOARDING", "DOWNLOAD_CHANGED", "FILL_SEARCH_TERM", "INIT", "MIGRATION_CANCEL", "MIGRATION_COMPLETED", "MIGRATION_START", "NEW_TAB_INIT", "NEW_TAB_INITIAL_STATE", "NEW_TAB_LOAD", "NEW_TAB_REHYDRATED", "NEW_TAB_STATE_REQUEST", "NEW_TAB_UNLOAD", "OPEN_DOWNLOAD_FILE", "OPEN_LINK", "OPEN_NEW_WINDOW", "OPEN_PRIVATE_WINDOW", "OPEN_WEBEXT_SETTINGS", "PAGE_PRERENDERED", "PLACES_BOOKMARK_ADDED", "PLACES_BOOKMARK_REMOVED", "PLACES_HISTORY_CLEARED", "PLACES_LINKS_CHANGED", "PLACES_LINK_BLOCKED", "PLACES_LINK_DELETED", "PLACES_SAVED_TO_POCKET", "POCKET_WAITING_FOR_SPOC", "PREFS_INITIAL_VALUES", "PREF_CHANGED", "PREVIEW_REQUEST", "PREVIEW_REQUEST_CANCEL", "PREVIEW_RESPONSE", "REMOVE_DOWNLOAD_FILE", "RICH_ICON_MISSING", "SAVE_SESSION_PERF_DATA", "SAVE_TO_POCKET", "SCREENSHOT_UPDATED", "SECTION_DEREGISTER", "SECTION_DISABLE", "SECTION_ENABLE", "SECTION_MOVE", "SECTION_OPTIONS_CHANGED", "SECTION_REGISTER", "SECTION_UPDATE", "SECTION_UPDATE_CARD", "SETTINGS_CLOSE", "SETTINGS_OPEN", "SET_PREF", "SHOW_DOWNLOAD_FILE", "SHOW_FIREFOX_ACCOUNTS", "SKIPPED_SIGNIN", "SNIPPETS_BLOCKLIST_CLEARED", "SNIPPETS_BLOCKLIST_UPDATED", "SNIPPETS_DATA", "SNIPPETS_RESET", "SNIPPET_BLOCKED", "SUBMIT_EMAIL", "SYSTEM_TICK", "TELEMETRY_IMPRESSION_STATS", "TELEMETRY_PERFORMANCE_EVENT", "TELEMETRY_UNDESIRED_EVENT", "TELEMETRY_USER_EVENT", "TOP_SITES_CANCEL_EDIT", "TOP_SITES_CLOSE_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_EDIT", "TOP_SITES_INSERT", "TOP_SITES_OPEN_SEARCH_SHORTCUTS_MODAL", "TOP_SITES_PIN", "TOP_SITES_PREFS_UPDATED", "TOP_SITES_UNPIN", "TOP_SITES_UPDATED", "TOTAL_BOOKMARKS_REQUEST", "TOTAL_BOOKMARKS_RESPONSE", "UNINIT", "UPDATE_PINNED_SEARCH_SHORTCUTS", "UPDATE_SEARCH_SHORTCUTS", "UPDATE_SECTION_PREFS", "WEBEXT_CLICK", "WEBEXT_DISMISS"]) {
|
||||
actionTypes[type] = type;
|
||||
}
|
||||
|
||||
|
@ -483,7 +489,6 @@ __webpack_require__.r(__webpack_exports__);
|
|||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SnippetsProvider", function() { return SnippetsProvider; });
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "addSnippetsSubscriber", function() { return addSnippetsSubscriber; });
|
||||
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(2);
|
||||
/* harmony import */ var content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(4);
|
||||
function _asyncToGenerator(fn) { return function () { var gen = fn.apply(this, arguments); return new Promise(function (resolve, reject) { function step(key, arg) { try { var info = gen[key](arg); var value = info.value; } catch (error) { reject(error); return; } if (info.done) { resolve(value); } else { return Promise.resolve(value).then(function (value) { step("next", value); }, function (err) { step("throw", err); }); } } return step("next"); }); }; }
|
||||
|
||||
const DATABASE_NAME = "snippets_db";
|
||||
|
@ -496,7 +501,6 @@ const SNIPPETS_DISABLED_EVENT = "Snippets:Disabled";
|
|||
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* SnippetsMap - A utility for cacheing values related to the snippet. It has
|
||||
* the same interface as a Map, but is optionally backed by
|
||||
|
@ -876,13 +880,18 @@ class SnippetsProvider {
|
|||
*/
|
||||
function addSnippetsSubscriber(store) {
|
||||
const snippets = new SnippetsProvider(store.dispatch);
|
||||
const asrouterContent = new content_src_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_1__["ASRouterContent"]();
|
||||
|
||||
let initializing = false;
|
||||
|
||||
store.subscribe(_asyncToGenerator(function* () {
|
||||
const state = store.getState();
|
||||
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort > 0;
|
||||
let snippetsEnabled = false;
|
||||
try {
|
||||
snippetsEnabled = JSON.parse(state.Prefs.values["asrouter.messageProviders"]).find(function (i) {
|
||||
return i.id === "snippets";
|
||||
}).enabled;
|
||||
} catch (e) {}
|
||||
const isASRouterEnabled = state.Prefs.values.asrouterExperimentEnabled && snippetsEnabled;
|
||||
// state.Prefs.values["feeds.snippets"]: Should snippets be shown?
|
||||
// state.Snippets.initialized Is the snippets data initialized?
|
||||
// snippets.initialized: Is SnippetsProvider currently initialised?
|
||||
|
@ -897,18 +906,10 @@ function addSnippetsSubscriber(store) {
|
|||
} else if ((state.Prefs.values["feeds.snippets"] === false || state.Prefs.values.disableSnippets === true) && snippets.initialized) {
|
||||
snippets.uninit();
|
||||
}
|
||||
|
||||
// Turn on AS Router snippets if the experiment is enabled and the snippets pref is on;
|
||||
// otherwise, turn it off.
|
||||
if ((state.Prefs.values.asrouterExperimentEnabled || state.Prefs.values.asrouterOnboardingCohort > 0) && state.Prefs.values["feeds.snippets"] && !asrouterContent.initialized) {
|
||||
asrouterContent.init();
|
||||
} else if ((!state.Prefs.values.asrouterExperimentEnabled && state.Prefs.values.asrouterOnboardingCohort === 0 || !state.Prefs.values["feeds.snippets"]) && asrouterContent.initialized) {
|
||||
asrouterContent.uninit();
|
||||
}
|
||||
}));
|
||||
|
||||
// These values are returned for testing purposes
|
||||
return { snippets, asrouterContent };
|
||||
// Returned for testing purposes
|
||||
return { snippets };
|
||||
}
|
||||
/* WEBPACK VAR INJECTION */}.call(this, __webpack_require__(1)))
|
||||
|
||||
|
@ -922,18 +923,18 @@ __webpack_require__.r(__webpack_exports__);
|
|||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "convertLinks", function() { return convertLinks; });
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterUISurface", function() { return ASRouterUISurface; });
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "ASRouterContent", function() { return ASRouterContent; });
|
||||
/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(40);
|
||||
/* harmony import */ var fluent_react__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(41);
|
||||
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(2);
|
||||
/* harmony import */ var content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(7);
|
||||
/* harmony import */ var _components_ImpressionsWrapper_ImpressionsWrapper__WEBPACK_IMPORTED_MODULE_3__ = __webpack_require__(9);
|
||||
/* harmony import */ var fluent__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(39);
|
||||
/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(43);
|
||||
/* harmony import */ var fluent__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(40);
|
||||
/* harmony import */ var _templates_OnboardingMessage_OnboardingMessage__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(44);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
|
||||
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(10);
|
||||
/* harmony import */ var react_dom__WEBPACK_IMPORTED_MODULE_7___default = /*#__PURE__*/__webpack_require__.n(react_dom__WEBPACK_IMPORTED_MODULE_7__);
|
||||
/* harmony import */ var _template_utils__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(11);
|
||||
/* harmony import */ var _templates_SimpleSnippet_SimpleSnippet__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(41);
|
||||
/* harmony import */ var _templates_SimpleSnippet_SimpleSnippet__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(42);
|
||||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||||
|
||||
|
||||
|
@ -1117,6 +1118,11 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
|
|||
this.setState({ message: {} });
|
||||
}
|
||||
break;
|
||||
case "CLEAR_PROVIDER":
|
||||
if (action.data.id === this.state.message.provider) {
|
||||
this.setState({ message: {} });
|
||||
}
|
||||
break;
|
||||
case "CLEAR_BUNDLE":
|
||||
if (this.state.bundle.bundle) {
|
||||
this.setState({ bundle: {} });
|
||||
|
@ -1631,7 +1637,7 @@ class _Base extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
|
|||
const { initialized } = App;
|
||||
|
||||
const prefs = props.Prefs.values;
|
||||
if ((prefs.asrouterExperimentEnabled || prefs.asrouterOnboardingCohort > 0) && window.location.hash === "#asrouter") {
|
||||
if (prefs.asrouterExperimentEnabled && window.location.hash === "#asrouter") {
|
||||
return react__WEBPACK_IMPORTED_MODULE_8___default.a.createElement(content_src_components_ASRouterAdmin_ASRouterAdmin__WEBPACK_IMPORTED_MODULE_2__["ASRouterAdmin"], null);
|
||||
}
|
||||
|
||||
|
@ -1641,7 +1647,12 @@ class _Base extends react__WEBPACK_IMPORTED_MODULE_8___default.a.PureComponent {
|
|||
|
||||
// Until we can delete the existing onboarding tour, just hide the onboarding button when users are in
|
||||
// the new simplified onboarding experiment. CSS hacks ftw
|
||||
if (prefs.asrouterOnboardingCohort > 0) {
|
||||
let isOnboardingEnabled = false;
|
||||
try {
|
||||
isOnboardingEnabled = JSON.parse(prefs["asrouter.messageProviders"]).find(i => i.id === "onboarding").enabled;
|
||||
} catch (e) {}
|
||||
|
||||
if (isOnboardingEnabled) {
|
||||
global.document.body.classList.add("hide-onboarding");
|
||||
}
|
||||
|
||||
|
@ -1808,8 +1819,8 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
|
|||
|
||||
renderMessageItem(msg) {
|
||||
const isCurrent = msg.id === this.state.lastMessageId;
|
||||
const isBlocked = this.state.blockList.includes(msg.id);
|
||||
const impressions = this.state.impressions[msg.id] ? this.state.impressions[msg.id].length : 0;
|
||||
const isBlocked = this.state.messageBlockList.includes(msg.id);
|
||||
const impressions = this.state.messageImpressions[msg.id] ? this.state.impressions[msg.id].length : 0;
|
||||
|
||||
let itemClassName = "message-item";
|
||||
if (isCurrent) {
|
||||
|
@ -1885,7 +1896,18 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
|
|||
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
||||
"tbody",
|
||||
null,
|
||||
this.state.providers.map((provider, i) => react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
||||
this.state.providers.map((provider, i) => {
|
||||
let label = "(local)";
|
||||
if (provider.type === "remote") {
|
||||
label = react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
|
||||
"a",
|
||||
{ target: "_blank", href: provider.url },
|
||||
provider.url
|
||||
);
|
||||
} 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(
|
||||
|
@ -1896,13 +1918,10 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
|
|||
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
|
||||
) : "(local)"
|
||||
label
|
||||
)
|
||||
))
|
||||
);
|
||||
})
|
||||
)
|
||||
);
|
||||
}
|
||||
|
@ -2461,7 +2480,7 @@ __webpack_require__.r(__webpack_exports__);
|
|||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "SectionIntl", function() { return SectionIntl; });
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "_Sections", function() { return _Sections; });
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "Sections", function() { return Sections; });
|
||||
/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(44);
|
||||
/* harmony import */ var content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(45);
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(13);
|
||||
/* harmony import */ var react_intl__WEBPACK_IMPORTED_MODULE_1___default = /*#__PURE__*/__webpack_require__.n(react_intl__WEBPACK_IMPORTED_MODULE_1__);
|
||||
/* harmony import */ var common_Actions_jsm__WEBPACK_IMPORTED_MODULE_2__ = __webpack_require__(2);
|
||||
|
@ -2607,6 +2626,7 @@ class Section extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent
|
|||
pref, privacyNoticeURL, isFirst, isLast
|
||||
} = this.props;
|
||||
|
||||
const waitingForSpoc = id === "topstories" && this.props.Pocket.waitingForSpoc;
|
||||
const maxCardsPerRow = compactCards ? CARDS_PER_ROW_COMPACT_WIDE : CARDS_PER_ROW_DEFAULT;
|
||||
const { numRows } = this;
|
||||
const maxCards = maxCardsPerRow * numRows;
|
||||
|
@ -2629,7 +2649,13 @@ class Section extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent
|
|||
// On narrow viewports, we only show 3 cards per row. We'll mark the rest as
|
||||
// .hide-for-narrow to hide in CSS via @media query.
|
||||
const className = i >= maxCardsOnNarrow ? "hide-for-narrow" : "";
|
||||
cards.push(link ? react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__["Card"], { key: i,
|
||||
let usePlaceholder = !link;
|
||||
// If we are in the third card and waiting for spoc,
|
||||
// use the placeholder.
|
||||
if (!usePlaceholder && i === 2 && waitingForSpoc) {
|
||||
usePlaceholder = true;
|
||||
}
|
||||
cards.push(!usePlaceholder ? react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(content_src_components_Card_Card__WEBPACK_IMPORTED_MODULE_0__["Card"], { key: i,
|
||||
index: i,
|
||||
className: className,
|
||||
dispatch: dispatch,
|
||||
|
@ -2696,7 +2722,7 @@ Section.defaultProps = {
|
|||
title: ""
|
||||
};
|
||||
|
||||
const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Prefs: state.Prefs }))(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(Section));
|
||||
const SectionIntl = Object(react_redux__WEBPACK_IMPORTED_MODULE_5__["connect"])(state => ({ Prefs: state.Prefs, Pocket: state.Pocket }))(Object(react_intl__WEBPACK_IMPORTED_MODULE_1__["injectIntl"])(Section));
|
||||
|
||||
class _Sections extends react__WEBPACK_IMPORTED_MODULE_6___default.a.PureComponent {
|
||||
renderSections() {
|
||||
|
@ -3996,8 +4022,8 @@ __webpack_require__.r(__webpack_exports__);
|
|||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(5);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_6___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_6__);
|
||||
/* harmony import */ var _SearchShortcutsForm__WEBPACK_IMPORTED_MODULE_7__ = __webpack_require__(35);
|
||||
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(42);
|
||||
/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(45);
|
||||
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_8__ = __webpack_require__(43);
|
||||
/* harmony import */ var _TopSiteForm__WEBPACK_IMPORTED_MODULE_9__ = __webpack_require__(46);
|
||||
/* harmony import */ var _TopSite__WEBPACK_IMPORTED_MODULE_10__ = __webpack_require__(36);
|
||||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||||
|
||||
|
@ -4405,7 +4431,7 @@ __webpack_require__.r(__webpack_exports__);
|
|||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4__ = __webpack_require__(5);
|
||||
/* harmony import */ var react__WEBPACK_IMPORTED_MODULE_4___default = /*#__PURE__*/__webpack_require__.n(react__WEBPACK_IMPORTED_MODULE_4__);
|
||||
/* harmony import */ var content_src_lib_screenshot_utils__WEBPACK_IMPORTED_MODULE_5__ = __webpack_require__(26);
|
||||
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(42);
|
||||
/* harmony import */ var common_Reducers_jsm__WEBPACK_IMPORTED_MODULE_6__ = __webpack_require__(43);
|
||||
var _extends = Object.assign || function (target) { for (var i = 1; i < arguments.length; i++) { var source = arguments[i]; for (var key in source) { if (Object.prototype.hasOwnProperty.call(source, key)) { target[key] = source[key]; } } } return target; };
|
||||
|
||||
|
||||
|
@ -4977,8 +5003,8 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
|
|||
if (_this.props.fxa_endpoint && !_this.didFetch) {
|
||||
try {
|
||||
_this.didFetch = true;
|
||||
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?entrypoint=
|
||||
activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email`);
|
||||
const fxaParams = "entrypoint=activity-stream-firstrun&utm_source=activity-stream&utm_campaign=firstrun&form_type=email";
|
||||
const response = yield fetch(`${_this.props.fxa_endpoint}/metrics-flow?${fxaParams}`);
|
||||
if (response.status === 200) {
|
||||
const { flowId, flowBeginTime } = yield response.json();
|
||||
_this.setState({ flowId, flowBeginTime });
|
||||
|
@ -5225,6 +5251,27 @@ class DetectUserSessionStart {
|
|||
/* 39 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
__webpack_require__.r(__webpack_exports__);
|
||||
/* harmony export (binding) */ __webpack_require__.d(__webpack_exports__, "enableASRouterContent", function() { return enableASRouterContent; });
|
||||
function enableASRouterContent(store, asrouterContent) {
|
||||
// Enable asrouter content
|
||||
store.subscribe(() => {
|
||||
const state = store.getState();
|
||||
if (state.Prefs.values.asrouterExperimentEnabled && !asrouterContent.initialized) {
|
||||
asrouterContent.init();
|
||||
} else if (!state.Prefs.values.asrouterExperimentEnabled && asrouterContent.initialized) {
|
||||
asrouterContent.uninit();
|
||||
}
|
||||
});
|
||||
// Return this for testing purposes
|
||||
return { asrouterContent };
|
||||
}
|
||||
|
||||
/***/ }),
|
||||
/* 40 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
||||
// CONCATENATED MODULE: ./node_modules/fluent/src/parser.js
|
||||
|
@ -7323,7 +7370,7 @@ function ftl(strings) {
|
|||
|
||||
|
||||
/***/ }),
|
||||
/* 40 */
|
||||
/* 41 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -7336,7 +7383,7 @@ var external_PropTypes_ = __webpack_require__(6);
|
|||
var external_PropTypes_default = /*#__PURE__*/__webpack_require__.n(external_PropTypes_);
|
||||
|
||||
// EXTERNAL MODULE: ./node_modules/fluent/src/index.js + 8 modules
|
||||
var src = __webpack_require__(39);
|
||||
var src = __webpack_require__(40);
|
||||
|
||||
// CONCATENATED MODULE: ./node_modules/fluent-react/src/localization.js
|
||||
|
||||
|
@ -7838,7 +7885,7 @@ localized_Localized.propTypes = {
|
|||
|
||||
|
||||
/***/ }),
|
||||
/* 41 */
|
||||
/* 42 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -7990,7 +8037,7 @@ class SimpleSnippet_SimpleSnippet extends external_React_default.a.PureComponent
|
|||
}
|
||||
|
||||
/***/ }),
|
||||
/* 42 */
|
||||
/* 43 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -8076,7 +8123,8 @@ const INITIAL_STATE = {
|
|||
visible: false,
|
||||
data: {}
|
||||
},
|
||||
Sections: []
|
||||
Sections: [],
|
||||
Pocket: { waitingForSpoc: true }
|
||||
};
|
||||
|
||||
|
||||
|
@ -8422,10 +8470,19 @@ function Snippets(prevState = INITIAL_STATE.Snippets, action) {
|
|||
}
|
||||
}
|
||||
|
||||
var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections };
|
||||
function Pocket(prevState = INITIAL_STATE.Pocket, action) {
|
||||
switch (action.type) {
|
||||
case Actions["actionTypes"].POCKET_WAITING_FOR_SPOC:
|
||||
return Object.assign({}, prevState, { waitingForSpoc: action.data });
|
||||
default:
|
||||
return prevState;
|
||||
}
|
||||
}
|
||||
|
||||
var reducers = { TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket };
|
||||
|
||||
/***/ }),
|
||||
/* 43 */
|
||||
/* 44 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -8496,7 +8553,13 @@ class OnboardingMessage_OnboardingCard extends external_React_default.a.PureComp
|
|||
|
||||
onClick() {
|
||||
const { props } = this;
|
||||
props.sendUserActionTelemetry({ event: "CLICK_BUTTON", message_id: props.id, id: props.UISurface });
|
||||
const ping = {
|
||||
event: "CLICK_BUTTON",
|
||||
message_id: props.id,
|
||||
id: props.UISurface,
|
||||
includeClientID: true
|
||||
};
|
||||
props.sendUserActionTelemetry(ping);
|
||||
props.onAction(props.content.button_action);
|
||||
}
|
||||
|
||||
|
@ -8563,7 +8626,7 @@ class OnboardingMessage_OnboardingMessage extends external_React_default.a.PureC
|
|||
}
|
||||
|
||||
/***/ }),
|
||||
/* 44 */
|
||||
/* 45 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
@ -8927,7 +8990,7 @@ const Card = Object(external_ReactRedux_["connect"])(state => ({ platform: state
|
|||
const PlaceholderCard = props => external_React_default.a.createElement(Card, { placeholder: true, className: props.className });
|
||||
|
||||
/***/ }),
|
||||
/* 45 */
|
||||
/* 46 */
|
||||
/***/ (function(module, __webpack_exports__, __webpack_require__) {
|
||||
|
||||
"use strict";
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.6 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.4 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 3.3 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.7 KiB |
Двоичные данные
browser/components/newtab/data/content/assets/cfr_reddit_enhancement.png
Normal file
Двоичные данные
browser/components/newtab/data/content/assets/cfr_reddit_enhancement.png
Normal file
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 6.4 KiB |
Двоичный файл не отображается.
После Ширина: | Высота: | Размер: 1.2 KiB |
|
@ -576,10 +576,11 @@ This reports the impression of Activity Stream Router.
|
|||
|
||||
This reports the user's interaction with Activity Stream Router.
|
||||
|
||||
#### Snippets interaction pings
|
||||
```js
|
||||
{
|
||||
"client_id": "n/a",
|
||||
"action": ["snippets_user_event" | "onboarding_user_event"],
|
||||
"action": "snippets_user_event",
|
||||
"addon_version": "20180710100040",
|
||||
"impression_id": "{005deed0-e3e4-4c02-a041-17405fd703f6}",
|
||||
"locale": "en-US",
|
||||
|
@ -589,6 +590,20 @@ This reports the user's interaction with Activity Stream Router.
|
|||
}
|
||||
```
|
||||
|
||||
#### Onboarding interaction pings
|
||||
```js
|
||||
{
|
||||
"client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
|
||||
"action": "onboarding_user_event",
|
||||
"addon_version": "20180710100040",
|
||||
"impression_id": "n/a",
|
||||
"locale": "en-US",
|
||||
"source": "NEWTAB_FOOTER_BAR",
|
||||
"message_id": "onboarding_message_1",
|
||||
"event": "CLICK_BUTTION"
|
||||
}
|
||||
```
|
||||
|
||||
### Targeting error pings
|
||||
|
||||
This reports when an error has occurred when parsing/evaluating a JEXL targeting string in a message.
|
||||
|
|
|
@ -142,7 +142,9 @@ module.exports = function(config) {
|
|||
path.resolve("vendor"),
|
||||
path.resolve("lib/ASRouterTargeting.jsm"),
|
||||
path.resolve("lib/ASRouterTriggerListeners.jsm"),
|
||||
path.resolve("lib/OnboardingMessageProvider.jsm")
|
||||
path.resolve("lib/OnboardingMessageProvider.jsm"),
|
||||
path.resolve("lib/CFRMessageProvider.jsm"),
|
||||
path.resolve("lib/CFRPageActions.jsm")
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -5,11 +5,16 @@
|
|||
|
||||
ChromeUtils.import("resource://gre/modules/Services.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
ChromeUtils.import("resource://gre/modules/AddonManager.jsm");
|
||||
ChromeUtils.import("resource:///modules/UITour.jsm");
|
||||
XPCOMUtils.defineLazyGlobalGetters(this, ["fetch"]);
|
||||
XPCOMUtils.defineLazyModuleGetters(this, {
|
||||
AddonManager: "resource://gre/modules/AddonManager.jsm",
|
||||
UITour: "resource:///modules/UITour.jsm"
|
||||
});
|
||||
const {ASRouterActions: ra, actionCreators: ac} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
|
||||
const {CFRMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/CFRMessageProvider.jsm", {});
|
||||
const {OnboardingMessageProvider} = ChromeUtils.import("resource://activity-stream/lib/OnboardingMessageProvider.jsm", {});
|
||||
const {RemoteSettings} = ChromeUtils.import("resource://services-settings/remote-settings.js", {});
|
||||
const {CFRPageActions} = ChromeUtils.import("resource://activity-stream/lib/CFRPageActions.jsm", {});
|
||||
|
||||
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
|
||||
"resource://activity-stream/lib/ASRouterTargeting.jsm");
|
||||
|
@ -19,6 +24,7 @@ ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
|
|||
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
|
||||
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
|
||||
const MESSAGE_PROVIDER_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
|
||||
const ONE_DAY_IN_MS = 24 * 60 * 60 * 1000;
|
||||
// List of hosts for endpoints that serve router messages.
|
||||
// Key is allowed host, value is a name for the endpoint host.
|
||||
const DEFAULT_WHITELIST_HOSTS = {
|
||||
|
@ -26,8 +32,10 @@ const DEFAULT_WHITELIST_HOSTS = {
|
|||
"snippets-admin.mozilla.org": "preview"
|
||||
};
|
||||
const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
|
||||
// Max possible impressions cap for any message
|
||||
const MAX_MESSAGE_LIFETIME_CAP = 100;
|
||||
|
||||
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider};
|
||||
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider, CFRMessageProvider};
|
||||
const STARTPAGE_VERSION = "0.1.0";
|
||||
|
||||
const MessageLoaderUtils = {
|
||||
|
@ -70,6 +78,29 @@ const MessageLoaderUtils = {
|
|||
return remoteMessages;
|
||||
},
|
||||
|
||||
/**
|
||||
* _remoteSettingsLoader - Loads messages for a RemoteSettings provider
|
||||
*
|
||||
* @param {obj} provider An AS router provider
|
||||
* @param {string} provider.bucket The name of the Remote Settings bucket
|
||||
* @returns {Promise} resolves with an array of messages, or an empty array if none could be fetched
|
||||
*/
|
||||
async _remoteSettingsLoader(provider) {
|
||||
let messages = [];
|
||||
if (provider.bucket) {
|
||||
try {
|
||||
messages = await MessageLoaderUtils._getRemoteSettingsMessages(provider.bucket);
|
||||
} catch (e) {
|
||||
Cu.reportError(e);
|
||||
}
|
||||
}
|
||||
return messages;
|
||||
},
|
||||
|
||||
_getRemoteSettingsMessages(bucket) {
|
||||
return RemoteSettings(bucket).get({filters: {locale: Services.locale.getAppLocaleAsLangTag()}});
|
||||
},
|
||||
|
||||
/**
|
||||
* _getMessageLoader - return the right loading function given the provider's type
|
||||
*
|
||||
|
@ -80,6 +111,8 @@ const MessageLoaderUtils = {
|
|||
switch (provider.type) {
|
||||
case "remote":
|
||||
return this._remoteLoader;
|
||||
case "remote-settings":
|
||||
return this._remoteSettingsLoader;
|
||||
case "local":
|
||||
default:
|
||||
return this._localLoader;
|
||||
|
@ -144,8 +177,10 @@ class _ASRouter {
|
|||
this._state = {
|
||||
lastMessageId: null,
|
||||
providers: [],
|
||||
blockList: [],
|
||||
impressions: {},
|
||||
messageBlockList: [],
|
||||
providerBlockList: [],
|
||||
messageImpressions: {},
|
||||
providerImpressions: {},
|
||||
messages: []
|
||||
};
|
||||
this._triggerHandler = this._triggerHandler.bind(this);
|
||||
|
@ -171,7 +206,11 @@ class _ASRouter {
|
|||
const providers = existingPreviewProvider ? [existingPreviewProvider] : [];
|
||||
const providersJSON = Services.prefs.getStringPref(this._messageProviderPref, "");
|
||||
try {
|
||||
JSON.parse(providersJSON).forEach(provider => providers.push(provider));
|
||||
JSON.parse(providersJSON).forEach(provider => {
|
||||
if (provider.enabled) {
|
||||
providers.push(provider);
|
||||
}
|
||||
});
|
||||
} catch (e) {
|
||||
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
|
||||
}
|
||||
|
@ -283,10 +322,13 @@ class _ASRouter {
|
|||
this._storage = storage;
|
||||
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
|
||||
this.dispatchToAS = dispatchToAS;
|
||||
this.dispatch = this.dispatch.bind(this);
|
||||
|
||||
const blockList = await this._storage.get("blockList") || [];
|
||||
const impressions = await this._storage.get("impressions") || {};
|
||||
await this.setState({blockList, impressions});
|
||||
const messageBlockList = await this._storage.get("messageBlockList") || [];
|
||||
const providerBlockList = await this._storage.get("providerBlockList") || [];
|
||||
const messageImpressions = await this._storage.get("messageImpressions") || {};
|
||||
const providerImpressions = await this._storage.get("providerImpressions") || {};
|
||||
await this.setState({messageBlockList, providerBlockList, messageImpressions, providerImpressions});
|
||||
this._updateMessageProviders();
|
||||
await this.loadMessagesFromAllProviders();
|
||||
|
||||
|
@ -336,18 +378,57 @@ class _ASRouter {
|
|||
}
|
||||
}
|
||||
|
||||
_findMessage(messages, target, trigger) {
|
||||
const {impressions} = this.state;
|
||||
_findMessage(candidateMessages, trigger) {
|
||||
const messages = candidateMessages.filter(m => this.isBelowFrequencyCaps(m));
|
||||
|
||||
// Find a message that matches the targeting context as well as the trigger context (if one is provided)
|
||||
// If no trigger is provided, we should find a message WITHOUT a trigger property defined.
|
||||
return ASRouterTargeting.findMatchingMessage({messages, impressions, trigger, onError: this._handleTargetingError});
|
||||
return ASRouterTargeting.findMatchingMessage({messages, trigger, onError: this._handleTargetingError});
|
||||
}
|
||||
|
||||
_orderBundle(bundle) {
|
||||
return bundle.sort((a, b) => a.order - b.order);
|
||||
}
|
||||
|
||||
// Work out if a message can be shown based on its and its provider's frequency caps.
|
||||
isBelowFrequencyCaps(message) {
|
||||
const {providers, messageImpressions, providerImpressions} = this.state;
|
||||
|
||||
const provider = providers.find(p => p.id === message.provider);
|
||||
const impressionsForMessage = messageImpressions[message.id];
|
||||
const impressionsForProvider = providerImpressions[message.provider];
|
||||
|
||||
return (this._isBelowItemFrequencyCap(message, impressionsForMessage, MAX_MESSAGE_LIFETIME_CAP) &&
|
||||
this._isBelowItemFrequencyCap(provider, impressionsForProvider));
|
||||
}
|
||||
|
||||
// Helper for isBelowFrecencyCaps - work out if the frequency cap for the given
|
||||
// item has been exceeded or not
|
||||
_isBelowItemFrequencyCap(item, impressions, maxLifetimeCap = Infinity) {
|
||||
if (item && item.frequency && impressions && impressions.length) {
|
||||
if (
|
||||
item.frequency.lifetime &&
|
||||
impressions.length >= Math.min(item.frequency.lifetime, maxLifetimeCap)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
if (item.frequency.custom) {
|
||||
const now = Date.now();
|
||||
for (const setting of item.frequency.custom) {
|
||||
let {period} = setting;
|
||||
if (period === "daily") {
|
||||
period = ONE_DAY_IN_MS;
|
||||
}
|
||||
const impressionsInPeriod = impressions.filter(t => (now - t) < period);
|
||||
if (impressionsInPeriod.length >= setting.cap) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
async _getBundledMessages(originalMessage, target, trigger, force = false) {
|
||||
let result = [{content: originalMessage.content, id: originalMessage.id, order: originalMessage.order || 0}];
|
||||
|
||||
|
@ -367,7 +448,7 @@ class _ASRouter {
|
|||
} else {
|
||||
while (bundledMessagesOfSameTemplate.length) {
|
||||
// Find a message that matches the targeting context - or break if there are no matching messages
|
||||
const message = await this._findMessage(bundledMessagesOfSameTemplate, target, trigger);
|
||||
const message = await this._findMessage(bundledMessagesOfSameTemplate, trigger);
|
||||
if (!message) {
|
||||
/* istanbul ignore next */ // Code coverage in mochitests
|
||||
break;
|
||||
|
@ -393,40 +474,61 @@ class _ASRouter {
|
|||
|
||||
_getUnblockedMessages() {
|
||||
let {state} = this;
|
||||
return state.messages.filter(item => !state.blockList.includes(item.id));
|
||||
return state.messages.filter(item =>
|
||||
!state.messageBlockList.includes(item.id) &&
|
||||
!state.providerBlockList.includes(item.provider)
|
||||
);
|
||||
}
|
||||
|
||||
async _sendMessageToTarget(message, target, trigger, force = false) {
|
||||
let bundledMessages;
|
||||
// If this message needs to be bundled with other messages of the same template, find them and bundle them together
|
||||
if (message && message.bundled) {
|
||||
bundledMessages = await this._getBundledMessages(message, target, trigger, force);
|
||||
}
|
||||
if (message && !message.bundled) {
|
||||
// If we only need to send 1 message, send the message
|
||||
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
|
||||
} else if (bundledMessages) {
|
||||
// If the message we want is bundled with other messages, send the entire bundle
|
||||
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: bundledMessages});
|
||||
} else {
|
||||
// No message is available, so send CLEAR_ALL.
|
||||
if (!message) {
|
||||
try {
|
||||
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_ALL"});
|
||||
} catch (e) {}
|
||||
|
||||
// For bundled messages, look for the rest of the bundle or else send CLEAR_ALL
|
||||
} else if (message.bundled) {
|
||||
const bundledMessages = await this._getBundledMessages(message, target, trigger, force);
|
||||
const action = bundledMessages ? {type: "SET_BUNDLED_MESSAGES", data: bundledMessages} : {type: "CLEAR_ALL"};
|
||||
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, action);
|
||||
|
||||
// CFR doorhanger
|
||||
} else if (message.template === "cfr_doorhanger") {
|
||||
CFRPageActions.addRecommendation(target, trigger.param, message, this.dispatch, force);
|
||||
|
||||
// New tab single messages
|
||||
} else {
|
||||
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "SET_MESSAGE", data: message});
|
||||
}
|
||||
}
|
||||
|
||||
async addImpression(message) {
|
||||
// Don't store impressions for messages that don't include any limits on frequency
|
||||
if (!message.frequency) {
|
||||
return;
|
||||
}
|
||||
const provider = this.state.providers.find(p => p.id === message.provider);
|
||||
// We only need to store impressions for messages that have frequency, or
|
||||
// that have providers that have frequency
|
||||
if (message.frequency || (provider && provider.frequency)) {
|
||||
const time = Date.now();
|
||||
await this.setState(state => {
|
||||
const messageImpressions = this._addImpressionForItem(state, message, "messageImpressions", time);
|
||||
const providerImpressions = this._addImpressionForItem(state, provider, "providerImpressions", time);
|
||||
return {messageImpressions, providerImpressions};
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// 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.impressions};
|
||||
impressions[message.id] = impressions[message.id] ? [...impressions[message.id]] : [];
|
||||
impressions[message.id].push(Date.now());
|
||||
this._storage.set("impressions", impressions);
|
||||
return {impressions};
|
||||
});
|
||||
const impressions = {...state[impressionsString]};
|
||||
if (item.frequency) {
|
||||
impressions[item.id] = impressions[item.id] ? [...impressions[item.id]] : [];
|
||||
impressions[item.id].push(time);
|
||||
this._storage.set(impressionsString, impressions);
|
||||
}
|
||||
return impressions;
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -447,20 +549,31 @@ class _ASRouter {
|
|||
/**
|
||||
* cleanupImpressions - this function cleans up obsolete impressions whenever
|
||||
* messages are refreshed or fetched. It will likely need to be more sophisticated in the future,
|
||||
* but the current behaviour for when impressions are cleared is as follows:
|
||||
* but the current behaviour for when both message impressions and provider impressions are
|
||||
* cleared is as follows (where `item` is either `message` or `provider`):
|
||||
*
|
||||
* 1. If the message id for a list of impressions no longer exists in state.messages, it will be cleared.
|
||||
* 2. If the message has time-bound frequency caps but no lifetime cap, any impressions older
|
||||
* 1. If the item id for a list of item impressions no longer exists in the ASRouter state, it
|
||||
* will be cleared.
|
||||
* 2. If the item has time-bound frequency caps but no lifetime cap, any item impressions older
|
||||
* than the longest time period will be cleared.
|
||||
*/
|
||||
async cleanupImpressions() {
|
||||
await this.setState(state => {
|
||||
const impressions = {...state.impressions};
|
||||
const messageImpressions = this._cleanupImpressionsForItems(state, state.messages, "messageImpressions");
|
||||
const providerImpressions = this._cleanupImpressionsForItems(state, state.providers, "providerImpressions");
|
||||
return {messageImpressions, providerImpressions};
|
||||
});
|
||||
}
|
||||
|
||||
// Helper for cleanupImpressions - calculate the updated impressions object for
|
||||
// the given items, then store it and return it
|
||||
_cleanupImpressionsForItems(state, items, impressionsString) {
|
||||
const impressions = {...state[impressionsString]};
|
||||
let needsUpdate = false;
|
||||
Object.keys(impressions).forEach(id => {
|
||||
const [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])) {
|
||||
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;
|
||||
|
@ -469,17 +582,16 @@ class _ASRouter {
|
|||
return;
|
||||
}
|
||||
// If we don't want to store impressions older than the longest period
|
||||
if (message.frequency.custom && !message.frequency.lifetime) {
|
||||
if (item.frequency.custom && !item.frequency.lifetime) {
|
||||
const now = Date.now();
|
||||
impressions[id] = impressions[id].filter(t => (now - t) < this.getLongestPeriod(message));
|
||||
impressions[id] = impressions[id].filter(t => (now - t) < this.getLongestPeriod(item));
|
||||
needsUpdate = true;
|
||||
}
|
||||
});
|
||||
if (needsUpdate) {
|
||||
this._storage.set("impressions", impressions);
|
||||
this._storage.set(impressionsString, impressions);
|
||||
}
|
||||
return {impressions};
|
||||
});
|
||||
return impressions;
|
||||
}
|
||||
|
||||
async sendNextMessage(target, trigger) {
|
||||
|
@ -490,7 +602,7 @@ class _ASRouter {
|
|||
if (previewMsgs.length) {
|
||||
[message] = previewMsgs;
|
||||
} else {
|
||||
message = await this._findMessage(msgs, target, trigger);
|
||||
message = await this._findMessage(msgs, trigger);
|
||||
}
|
||||
|
||||
if (previewMsgs.length) {
|
||||
|
@ -512,30 +624,30 @@ class _ASRouter {
|
|||
await this._sendMessageToTarget(newMessage, target, force, action.data);
|
||||
}
|
||||
|
||||
async blockById(idOrIds) {
|
||||
async blockMessageById(idOrIds) {
|
||||
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
||||
|
||||
await this.setState(state => {
|
||||
const blockList = [...state.blockList, ...idsToBlock];
|
||||
const messageBlockList = [...state.messageBlockList, ...idsToBlock];
|
||||
// When a message is blocked, its impressions should be cleared as well
|
||||
const impressions = {...state.impressions};
|
||||
idsToBlock.forEach(id => delete impressions[id]);
|
||||
this._storage.set("blockList", blockList);
|
||||
return {blockList, impressions};
|
||||
const messageImpressions = {...state.messageImpressions};
|
||||
idsToBlock.forEach(id => delete messageImpressions[id]);
|
||||
this._storage.set("messageBlockList", messageBlockList);
|
||||
return {messageBlockList, messageImpressions};
|
||||
});
|
||||
}
|
||||
|
||||
openLinkIn(url, target, {isPrivate = false, trusted = false, where = ""}) {
|
||||
const win = target.browser.ownerGlobal;
|
||||
const params = {
|
||||
private: isPrivate,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})
|
||||
};
|
||||
if (trusted) {
|
||||
win.openTrustedLinkIn(url, where);
|
||||
} else {
|
||||
win.openLinkIn(url, where, params);
|
||||
}
|
||||
async blockProviderById(idOrIds) {
|
||||
const idsToBlock = Array.isArray(idOrIds) ? idOrIds : [idOrIds];
|
||||
|
||||
await this.setState(state => {
|
||||
const providerBlockList = [...state.providerBlockList, ...idsToBlock];
|
||||
// When a provider is blocked, its impressions should be cleared as well
|
||||
const providerImpressions = {...state.providerImpressions};
|
||||
idsToBlock.forEach(id => delete providerImpressions[id]);
|
||||
this._storage.set("providerBlockList", providerBlockList);
|
||||
return {providerBlockList, providerImpressions};
|
||||
});
|
||||
}
|
||||
|
||||
_validPreviewEndpoint(url) {
|
||||
|
@ -579,7 +691,7 @@ class _ASRouter {
|
|||
|
||||
// To be passed to ASRouterTriggerListeners
|
||||
async _triggerHandler(target, trigger) {
|
||||
await this.onMessage({target, data: {type: "TRIGGER", trigger}});
|
||||
await this.onMessage({target, data: {type: "TRIGGER", data: {trigger}}});
|
||||
}
|
||||
|
||||
_removePreviewEndpoint(state) {
|
||||
|
@ -602,19 +714,13 @@ class _ASRouter {
|
|||
target.browser.ownerGlobal.OpenBrowserWindow({private: true});
|
||||
break;
|
||||
case ra.OPEN_URL:
|
||||
this.openLinkIn(action.data.url, target, {
|
||||
isPrivate: false,
|
||||
where: "tabshifted",
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getNullPrincipal({})
|
||||
target.browser.ownerGlobal.openLinkIn(action.data.url, "tabshifted", {
|
||||
private: false,
|
||||
triggeringPrincipal: Services.scriptSecurityManager.createNullPrincipal({})
|
||||
});
|
||||
break;
|
||||
case ra.OPEN_ABOUT_PAGE:
|
||||
this.openLinkIn(`about:${action.data.page}`, target, {
|
||||
isPrivate: false,
|
||||
trusted: true,
|
||||
where: "tab",
|
||||
triggeringPrincipal: Services.scriptSecurityManager.getSystemPrincipal(),
|
||||
});
|
||||
target.browser.ownerGlobal.openTrustedLinkIn(`about:${action.data.page}`, "tab");
|
||||
break;
|
||||
case ra.OPEN_APPLICATIONS_MENU:
|
||||
UITour.showMenu(target.browser.ownerGlobal, action.data.target);
|
||||
|
@ -625,6 +731,10 @@ class _ASRouter {
|
|||
}
|
||||
}
|
||||
|
||||
dispatch(action, target) {
|
||||
this.onMessage({data: action, target});
|
||||
}
|
||||
|
||||
async onMessage({data: action, target}) {
|
||||
switch (action.type) {
|
||||
case "USER_ACTION":
|
||||
|
@ -645,29 +755,41 @@ class _ASRouter {
|
|||
await this.sendNextMessage(target, (action.data && action.data.trigger) || {});
|
||||
break;
|
||||
case "BLOCK_MESSAGE_BY_ID":
|
||||
await this.blockById(action.data.id);
|
||||
await this.blockMessageById(action.data.id);
|
||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: action.data.id}});
|
||||
break;
|
||||
case "BLOCK_PROVIDER_BY_ID":
|
||||
await this.blockProviderById(action.data.id);
|
||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: action.data.id}});
|
||||
break;
|
||||
case "BLOCK_BUNDLE":
|
||||
await this.blockById(action.data.bundle.map(b => b.id));
|
||||
await this.blockMessageById(action.data.bundle.map(b => b.id));
|
||||
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
||||
break;
|
||||
case "UNBLOCK_MESSAGE_BY_ID":
|
||||
await this.setState(state => {
|
||||
const blockList = [...state.blockList];
|
||||
blockList.splice(blockList.indexOf(action.data.id), 1);
|
||||
this._storage.set("blockList", blockList);
|
||||
return {blockList};
|
||||
const messageBlockList = [...state.messageBlockList];
|
||||
messageBlockList.splice(messageBlockList.indexOf(action.data.id), 1);
|
||||
this._storage.set("messageBlockList", messageBlockList);
|
||||
return {messageBlockList};
|
||||
});
|
||||
break;
|
||||
case "UNBLOCK_PROVIDER_BY_ID":
|
||||
await this.setState(state => {
|
||||
const providerBlockList = [...state.providerBlockList];
|
||||
providerBlockList.splice(providerBlockList.indexOf(action.data.id), 1);
|
||||
this._storage.set("providerBlockList", providerBlockList);
|
||||
return {providerBlockList};
|
||||
});
|
||||
break;
|
||||
case "UNBLOCK_BUNDLE":
|
||||
await this.setState(state => {
|
||||
const blockList = [...state.blockList];
|
||||
const messageBlockList = [...state.messageBlockList];
|
||||
for (let message of action.data.bundle) {
|
||||
blockList.splice(blockList.indexOf(message.id), 1);
|
||||
messageBlockList.splice(messageBlockList.indexOf(message.id), 1);
|
||||
}
|
||||
this._storage.set("blockList", blockList);
|
||||
return {blockList};
|
||||
this._storage.set("messageBlockList", messageBlockList);
|
||||
return {messageBlockList};
|
||||
});
|
||||
break;
|
||||
case "OVERRIDE_MESSAGE":
|
||||
|
@ -682,7 +804,7 @@ class _ASRouter {
|
|||
}
|
||||
break;
|
||||
case "IMPRESSION":
|
||||
this.addImpression(action.data);
|
||||
await this.addImpression(action.data);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -39,10 +39,9 @@ class ASRouterFeed {
|
|||
enableOrDisableBasedOnPref() {
|
||||
const prefs = this.store.getState().Prefs.values;
|
||||
const isExperimentEnabled = prefs.asrouterExperimentEnabled;
|
||||
const isOnboardingExperimentEnabled = prefs.asrouterOnboardingCohort;
|
||||
if (!this.router.initialized && (isExperimentEnabled || isOnboardingExperimentEnabled > 0)) {
|
||||
if (!this.router.initialized && isExperimentEnabled) {
|
||||
this.enable();
|
||||
} else if ((!isExperimentEnabled || isOnboardingExperimentEnabled === 0) && this.router.initialized) {
|
||||
} else if (!isExperimentEnabled && this.router.initialized) {
|
||||
this.disable();
|
||||
}
|
||||
}
|
||||
|
@ -53,7 +52,7 @@ class ASRouterFeed {
|
|||
this.enableOrDisableBasedOnPref();
|
||||
break;
|
||||
case at.PREF_CHANGED:
|
||||
if (["asrouterOnboardingCohort", "asrouterExperimentEnabled"].includes(action.data.name)) {
|
||||
if (action.data.name === "asrouterExperimentEnabled") {
|
||||
this.enableOrDisableBasedOnPref();
|
||||
}
|
||||
break;
|
||||
|
|
|
@ -13,13 +13,9 @@ ChromeUtils.defineModuleGetter(this, "TelemetryEnvironment",
|
|||
"resource://gre/modules/TelemetryEnvironment.jsm");
|
||||
|
||||
const FXA_USERNAME_PREF = "services.sync.username";
|
||||
const ONBOARDING_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouterOnboardingCohort";
|
||||
const ONBOARDING_MESSAGE_PROVDIER_EXPERIMENT_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
|
||||
const MOZ_JEXL_FILEPATH = "mozjexl";
|
||||
|
||||
// Max possible cap for any message
|
||||
const MAX_LIFETIME_CAP = 100;
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
const {activityStreamProvider: asProvider} = NewTabUtils;
|
||||
|
||||
const FRECENT_SITES_UPDATE_INTERVAL = 6 * 60 * 60 * 1000; // Six hours
|
||||
|
@ -71,6 +67,9 @@ const TargetingGetters = {
|
|||
update: settings.update
|
||||
};
|
||||
},
|
||||
get currentDate() {
|
||||
return new Date();
|
||||
},
|
||||
get profileAgeCreated() {
|
||||
return new ProfileAge(null, null).created;
|
||||
},
|
||||
|
@ -109,7 +108,6 @@ const TargetingGetters = {
|
|||
return {addons: info, isFullData: fullData};
|
||||
});
|
||||
},
|
||||
|
||||
get searchEngines() {
|
||||
return new Promise(resolve => {
|
||||
// Note: calling init ensures this code is only executed after Search has been initialized
|
||||
|
@ -128,18 +126,15 @@ const TargetingGetters = {
|
|||
});
|
||||
});
|
||||
},
|
||||
|
||||
get isDefaultBrowser() {
|
||||
try {
|
||||
return ShellService.isDefaultBrowser();
|
||||
} catch (e) {}
|
||||
return null;
|
||||
},
|
||||
|
||||
get devToolsOpenedCount() {
|
||||
return Services.prefs.getIntPref("devtools.selfxss.count");
|
||||
},
|
||||
|
||||
get topFrecentSites() {
|
||||
return TopFrecentSitesCache.topFrecentSites.then(sites => sites.map(site => (
|
||||
{
|
||||
|
@ -150,10 +145,16 @@ const TargetingGetters = {
|
|||
}
|
||||
)));
|
||||
},
|
||||
|
||||
// Temporary targeting function for the purposes of running the simplified onboarding experience
|
||||
get isInExperimentCohort() {
|
||||
return Services.prefs.getIntPref(ONBOARDING_EXPERIMENT_PREF, 0);
|
||||
const allProviders = Services.prefs.getStringPref(ONBOARDING_MESSAGE_PROVDIER_EXPERIMENT_PREF, "");
|
||||
try {
|
||||
const {cohort} = JSON.parse(allProviders).find(i => i.id === "onboarding");
|
||||
return (typeof cohort === "number" ? cohort : 0);
|
||||
} catch (e) {
|
||||
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
};
|
||||
|
||||
|
@ -178,35 +179,6 @@ this.ASRouterTargeting = {
|
|||
return candidateMessageTrigger.params.includes(trigger.param);
|
||||
},
|
||||
|
||||
isBelowFrequencyCap(message, impressionsForMessage) {
|
||||
if (!message.frequency || !impressionsForMessage || !impressionsForMessage.length) {
|
||||
return true;
|
||||
}
|
||||
|
||||
if (
|
||||
message.frequency.lifetime &&
|
||||
impressionsForMessage.length >= Math.min(message.frequency.lifetime, MAX_LIFETIME_CAP)
|
||||
) {
|
||||
return false;
|
||||
}
|
||||
|
||||
if (message.frequency.custom) {
|
||||
const now = Date.now();
|
||||
for (const setting of message.frequency.custom) {
|
||||
let {period} = setting;
|
||||
if (period === "daily") {
|
||||
period = ONE_DAY;
|
||||
}
|
||||
const impressionsInPeriod = impressionsForMessage.filter(t => (now - t) < period);
|
||||
if (impressionsInPeriod.length >= setting.cap) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return true;
|
||||
},
|
||||
|
||||
/**
|
||||
* checkMessageTargeting - Checks is a message's targeting parameters are satisfied
|
||||
*
|
||||
|
@ -244,7 +216,7 @@ this.ASRouterTargeting = {
|
|||
* @param {obj|null} context A FilterExpression context. Defaults to TargetingGetters above.
|
||||
* @returns {obj} an AS router message
|
||||
*/
|
||||
async findMatchingMessage({messages, impressions = {}, trigger, context, onError}) {
|
||||
async findMatchingMessage({messages, trigger, context, onError}) {
|
||||
const arrayOfItems = [...messages];
|
||||
let match;
|
||||
let candidate;
|
||||
|
@ -255,7 +227,6 @@ this.ASRouterTargeting = {
|
|||
if (
|
||||
candidate &&
|
||||
(trigger ? this.isTriggerMatch(trigger, candidate.trigger) : !candidate.trigger) &&
|
||||
this.isBelowFrequencyCap(candidate, impressions[candidate.id]) &&
|
||||
// If a trigger expression was passed to this function, the message should match it.
|
||||
// Otherwise, we should choose a message with no trigger property (i.e. a message that can show up at any time)
|
||||
await this.checkMessageTargeting(candidate, context, onError)
|
||||
|
|
|
@ -77,7 +77,7 @@ this.ASRouterTriggerListeners = new Map([
|
|||
try {
|
||||
const host = (new URL(location)).hostname;
|
||||
if (this._hosts.has(host)) {
|
||||
this._triggerHandler(aBrowser.messageManager, {id: "openURL", param: host});
|
||||
this._triggerHandler(aBrowser, {id: "openURL", param: host});
|
||||
}
|
||||
} catch (e) {} // Couldn't parse location URL
|
||||
}
|
||||
|
|
|
@ -192,10 +192,6 @@ const PREFS_CONFIG = new Map([
|
|||
title: "Is the message center experiment on?",
|
||||
value: false
|
||||
}],
|
||||
["asrouterOnboardingCohort", {
|
||||
title: "What cohort is the user in?",
|
||||
value: 0
|
||||
}],
|
||||
["asrouter.messageProviders", {
|
||||
title: "Configuration for ASRouter message providers",
|
||||
|
||||
|
@ -203,16 +199,26 @@ const PREFS_CONFIG = new Map([
|
|||
* Each provider must have a unique id and a type of "local" or "remote".
|
||||
* Local providers must specify the name of an ASRouter message provider.
|
||||
* Remote providers must specify a `url` and an `updateCycleInMs`.
|
||||
* Each provider must also have an `enabled` boolean.
|
||||
*/
|
||||
value: JSON.stringify([{
|
||||
id: "onboarding",
|
||||
type: "local",
|
||||
localProvider: "OnboardingMessageProvider"
|
||||
localProvider: "OnboardingMessageProvider",
|
||||
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release",
|
||||
cohort: 0
|
||||
}, {
|
||||
id: "snippets",
|
||||
type: "remote",
|
||||
url: "https://activity-stream-icons.services.mozilla.com/v1/messages.json.br",
|
||||
updateCycleInMs: ONE_HOUR_IN_MS * 4
|
||||
url: "https://snippets.cdn.mozilla.net/us-west/bundles/bundle_d6d90fb9098ce8b45e60acf601bcb91b68322309.json",
|
||||
updateCycleInMs: ONE_HOUR_IN_MS * 4,
|
||||
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release"
|
||||
}, {
|
||||
id: "cfr",
|
||||
type: "local",
|
||||
localProvider: "CFRMessageProvider",
|
||||
enabled: AppConstants.MOZ_UPDATE_CHANNEL !== "release",
|
||||
cohort: 0
|
||||
}])
|
||||
}]
|
||||
]);
|
||||
|
|
|
@ -0,0 +1,286 @@
|
|||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this
|
||||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
"use strict";
|
||||
const BASE_ADDONS_DOWNLOAD_URL = "https://addons.mozilla.org/firefox/downloads/file";
|
||||
const AMAZON_ASSISTANT_PARAMS = {
|
||||
existing_addons: ["abb@amazon.com", "{75c7fe97-5a90-4b54-9052-3534235eaf41}", "{ef34596e-1e43-4e84-b2ff-1e58e287e08d}", "{ea280feb-155a-492e-8016-ac96dd995f2c}", "izer@camelcamelcamel.com", "amptra@keepa.com", "pricealarm@icopron.ch", "{774f76c7-6807-481e-bf64-f9b7d5cda602}"],
|
||||
open_urls: ["smile.amazon.com", "www.audible.com", "www.amazon.com", "amazon.com", "audible.com"],
|
||||
sumo_path: "extensionpromotions"
|
||||
};
|
||||
const FACEBOOK_CONTAINER_PARAMS = {
|
||||
existing_addons: ["@contain-facebook", "{bb1b80be-e6b3-40a1-9b6e-9d4073343f0b}", "{a50d61ca-d27b-437a-8b52-5fd801a0a88b}"],
|
||||
open_urls: ["www.facebook.com", "facebook.com"],
|
||||
sumo_path: "extensionrecommendations"
|
||||
};
|
||||
const GOOGLE_TRANSLATE_PARAMS = {
|
||||
existing_addons: ["jid1-93WyvpgvxzGATw@jetpack", "{087ef4e1-4286-4be6-9aa3-8d6c420ee1db}", "{4170faaa-ee87-4a0e-b57a-1aec49282887}", "jid1-TMndP6cdKgxLcQ@jetpack",
|
||||
"s3google@translator", "{9c63d15c-b4d9-43bd-b223-37f0a1f22e2a}", "translator@zoli.bod", "{8cda9ce6-7893-4f47-ac70-a65215cec288}", "simple-translate@sienori", "@translatenow",
|
||||
"{a79fafce-8da6-4685-923f-7ba1015b8748})", "{8a802b5a-eeab-11e2-a41d-b0096288709b}", "jid0-fbHwsGfb6kJyq2hj65KnbGte3yT@jetpack", "storetranslate.plugin@gmail.com",
|
||||
"jid1-r2tWDbSkq8AZK1@jetpack", "{b384b75c-c978-4c4d-b3cf-62a82d8f8f12}", "jid1-f7dnBeTj8ElpWQ@jetpack", "{dac8a935-4775-4918-9205-5c0600087dc4}", "gtranslation2@slam.com",
|
||||
"{e20e0de5-1667-4df4-bd69-705720e37391}", "{09e26ae9-e9c1-477c-80a6-99934212f2fe}", "mgxtranslator@magemagix.com", "gtranslatewins@mozilla.org"],
|
||||
open_urls: ["translate.google.com"],
|
||||
sumo_path: "extensionrecommendations"
|
||||
};
|
||||
const YOUTUBE_ENHANCE_PARAMS = {
|
||||
existing_addons: ["enhancerforyoutube@maximerf.addons.mozilla.org", "{dc8f61ab-5e98-4027-98ef-bb2ff6060d71}", "{7b1bf0b6-a1b9-42b0-b75d-252036438bdc}", "jid0-UVAeBCfd34Kk5usS8A1CBiobvM8@jetpack",
|
||||
"iridium@particlecore.github.io", "jid1-ss6kLNCbNz6u0g@jetpack", "{1cf918d2-f4ea-4b4f-b34e-455283fef19f}"],
|
||||
open_urls: ["www.youtube.com", "youtube.com"],
|
||||
sumo_path: "extensionrecommendations"
|
||||
};
|
||||
const WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS = {
|
||||
existing_addons: ["@wikipediacontextmenusearch", "{ebf47fc8-01d8-4dba-aa04-2118402f4b20}", "{5737a280-b359-4e26-95b0-adec5915a854}", "olivier.debroqueville@gmail.com", "{3923146e-98cb-472b-9c13-f6849d34d6b8}"],
|
||||
open_urls: ["www.wikipedia.org", "wikipedia.org"],
|
||||
sumo_path: "extensionrecommendations"
|
||||
};
|
||||
const REDDIT_ENHANCEMENT_PARAMS = {
|
||||
existing_addons: ["jid1-xUfzOsOFlzSOXg@jetpack"],
|
||||
open_urls: ["www.reddit.com", "reddit.com"],
|
||||
sumo_path: "extensionrecommendations"
|
||||
};
|
||||
|
||||
const CFR_MESSAGES = [
|
||||
{
|
||||
id: "AMAZON_ASSISTANT_1",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
notification_text: "Recommendation",
|
||||
heading_text: "Recommended Extension",
|
||||
info_icon: {
|
||||
label: "why_seeing_this",
|
||||
sumo_path: AMAZON_ASSISTANT_PARAMS.sumo_path
|
||||
},
|
||||
addon: {
|
||||
title: "Amazon Assistant",
|
||||
icon: "resource://activity-stream/data/content/assets/cfr_amazon_assistant.png",
|
||||
author: "Amazon",
|
||||
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/amazon-browser-bar/"
|
||||
},
|
||||
text: "Amazon Assistant helps you make better shopping decisions by showing product comparisons at thousands of retail sites.",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: "Add to Firefox",
|
||||
accessKey: "A",
|
||||
action: {
|
||||
type: "INSTALL_ADDON_FROM_URL",
|
||||
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/950930/amazon_assistant_for_firefox-10.1805.2.1019-an+fx.xpi`}
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
label: "No Thanks",
|
||||
accessKey: "N",
|
||||
action: {type: "CANCEL"}
|
||||
}
|
||||
}
|
||||
},
|
||||
frequency: {lifetime: 1},
|
||||
targeting: `
|
||||
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
|
||||
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
|
||||
trigger: {id: "openURL", params: AMAZON_ASSISTANT_PARAMS.open_urls}
|
||||
},
|
||||
{
|
||||
id: "FACEBOOK_CONTAINER_1",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
notification_text: "Recommendation",
|
||||
heading_text: "Recommended Extension",
|
||||
info_icon: {
|
||||
label: "why_seeing_this",
|
||||
sumo_path: FACEBOOK_CONTAINER_PARAMS.sumo_path
|
||||
},
|
||||
addon: {
|
||||
title: "Facebook Container",
|
||||
icon: "resource://activity-stream/data/content/assets/cfr_fb_container.png",
|
||||
author: "Mozilla",
|
||||
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/facebook-container/"
|
||||
},
|
||||
text: "Stop Facebook from tracking your activity across the web. Use Facebook the way you normally do without annoying ads following you around.",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: "Add to Firefox",
|
||||
accessKey: "A",
|
||||
action: {
|
||||
type: "INSTALL_ADDON_FROM_URL",
|
||||
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/918624/facebook_container-1.3.1-an+fx-linux.xpi`}
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
label: "No Thanks",
|
||||
accessKey: "N",
|
||||
action: {type: "CANCEL"}
|
||||
}
|
||||
}
|
||||
},
|
||||
frequency: {lifetime: 1},
|
||||
targeting: `
|
||||
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
|
||||
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
|
||||
trigger: {id: "openURL", params: FACEBOOK_CONTAINER_PARAMS.open_urls}
|
||||
},
|
||||
{
|
||||
id: "GOOGLE_TRANSLATE_1",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
notification_text: "Recommendation",
|
||||
heading_text: "Recommended Extension",
|
||||
info_icon: {
|
||||
label: "why_seeing_this",
|
||||
sumo_path: GOOGLE_TRANSLATE_PARAMS.sumo_path
|
||||
},
|
||||
addon: {
|
||||
title: "To Google Translate",
|
||||
icon: "resource://activity-stream/data/content/assets/cfr_google_translate.png",
|
||||
author: "Juan Escobar",
|
||||
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/to-google-translate/"
|
||||
},
|
||||
text: "Instantly translate any webpage text. Simply highlight the text, right-click to open the context menu, and choose a text or aural translation.",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: "Add to Firefox",
|
||||
accessKey: "A",
|
||||
action: {
|
||||
type: "INSTALL_ADDON_FROM_URL",
|
||||
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/1008798/al_traductor_de_google-3.3-an+fx.xpi`}
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
label: "No Thanks",
|
||||
accessKey: "N",
|
||||
action: {type: "CANCEL"}
|
||||
}
|
||||
}
|
||||
},
|
||||
frequency: {lifetime: 1},
|
||||
targeting: `
|
||||
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
|
||||
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
|
||||
trigger: {id: "openURL", params: GOOGLE_TRANSLATE_PARAMS.open_urls}
|
||||
},
|
||||
{
|
||||
id: "YOUTUBE_ENHANCE_1",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
notification_text: "Recommendation",
|
||||
heading_text: "Recommended Extension",
|
||||
info_icon: {
|
||||
label: "why_seeing_this",
|
||||
sumo_path: YOUTUBE_ENHANCE_PARAMS.sumo_path
|
||||
},
|
||||
addon: {
|
||||
title: "Enhancer for YouTube\u2122",
|
||||
icon: "resource://activity-stream/data/content/assets/cfr_enhancer_youtube.png",
|
||||
author: "Maxime RF",
|
||||
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/enhancer-for-youtube/"
|
||||
},
|
||||
text: "Take control of your YouTube experience. Automatically block annoying ads, set playback speed and volume, remove annotations, and more.",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: "Add to Firefox",
|
||||
accessKey: "A",
|
||||
action: {
|
||||
type: "INSTALL_ADDON_FROM_URL",
|
||||
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/1028400/enhancer_for_youtubetm-2.0.73-an+fx-linux.xpi`}
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
label: "No Thanks",
|
||||
accessKey: "N",
|
||||
action: {type: "CANCEL"}
|
||||
}
|
||||
}
|
||||
},
|
||||
frequency: {lifetime: 1},
|
||||
targeting: `
|
||||
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
|
||||
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
|
||||
trigger: {id: "openURL", params: YOUTUBE_ENHANCE_PARAMS.open_urls}
|
||||
},
|
||||
{
|
||||
id: "WIKIPEDIA_CONTEXT_MENU_SEARCH_1",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
notification_text: "Recommendation",
|
||||
heading_text: "Recommended Extension",
|
||||
info_icon: {
|
||||
label: "why_seeing_this",
|
||||
sumo_path: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.sumo_path
|
||||
},
|
||||
addon: {
|
||||
title: "Wikipedia Context Menu Search",
|
||||
icon: "resource://activity-stream/data/content/assets/cfr_wiki_search.png",
|
||||
author: "Nick Diedrich",
|
||||
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/wikipedia-context-menu-search/"
|
||||
},
|
||||
text: "Get to a Wikipedia page fast, from anywhere on the web. Just highlight any webpage text and right-click to open the context menu to start a Wikipedia search.",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: "Add to Firefox",
|
||||
accessKey: "A",
|
||||
action: {
|
||||
type: "INSTALL_ADDON_FROM_URL",
|
||||
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/890224/wikipedia_context_menu_search-1.8-an+fx.xpi`}
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
label: "No Thanks",
|
||||
accessKey: "N",
|
||||
action: {type: "CANCEL"}
|
||||
}
|
||||
}
|
||||
},
|
||||
frequency: {lifetime: 1},
|
||||
targeting: `
|
||||
(${JSON.stringify(WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
|
||||
(${JSON.stringify(WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
|
||||
trigger: {id: "openURL", params: WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.open_urls}
|
||||
},
|
||||
{
|
||||
id: "REDDIT_ENHANCEMENT_1",
|
||||
template: "cfr_doorhanger",
|
||||
content: {
|
||||
notification_text: "Recommendation",
|
||||
heading_text: "Recommended Extension",
|
||||
info_icon: {
|
||||
label: "why_seeing_this",
|
||||
sumo_path: REDDIT_ENHANCEMENT_PARAMS.sumo_path
|
||||
},
|
||||
addon: {
|
||||
title: "Reddit Enhancement Suite",
|
||||
icon: "resource://activity-stream/data/content/assets/cfr_reddit_enhancement.png",
|
||||
author: "honestbleeps",
|
||||
amo_url: "https://addons.mozilla.org/en-US/firefox/addon/reddit-enhancement-suite/"
|
||||
},
|
||||
text: "New features include Inline Image Viewer, Never Ending Reddit (never click 'next page' again), Keyboard Navigation, Account Switcher, and User Tagger.",
|
||||
buttons: {
|
||||
primary: {
|
||||
label: "Add to Firefox",
|
||||
accessKey: "A",
|
||||
action: {
|
||||
type: "INSTALL_ADDON_FROM_URL",
|
||||
data: {url: `${BASE_ADDONS_DOWNLOAD_URL}/991623/reddit_enhancement_suite-5.12.5-an+fx.xpi`}
|
||||
}
|
||||
},
|
||||
secondary: {
|
||||
label: "No Thanks",
|
||||
accessKey: "N",
|
||||
action: {type: "CANCEL"}
|
||||
}
|
||||
}
|
||||
},
|
||||
frequency: {lifetime: 1},
|
||||
targeting: `
|
||||
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
|
||||
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.open_urls)} intersect topFrecentSites|mapToProperty('host'))|length > 0`,
|
||||
trigger: {id: "openURL", params: REDDIT_ENHANCEMENT_PARAMS.open_urls}
|
||||
}
|
||||
];
|
||||
|
||||
const CFRMessageProvider = {
|
||||
getMessages() {
|
||||
return CFR_MESSAGES;
|
||||
}
|
||||
};
|
||||
this.CFRMessageProvider = CFRMessageProvider;
|
||||
|
||||
const EXPORTED_SYMBOLS = ["CFRMessageProvider"];
|
|
@ -34,10 +34,14 @@ class PageAction {
|
|||
this.button = win.document.getElementById("cfr-button");
|
||||
this.label = win.document.getElementById("cfr-label");
|
||||
|
||||
// This should NOT be use directly to dispatch message-defined actions attached to buttons.
|
||||
// Please use dispatchUserAction instead.
|
||||
this._dispatchToASRouter = dispatchToASRouter;
|
||||
|
||||
this._popupStateChange = this._popupStateChange.bind(this);
|
||||
this._collapse = this._collapse.bind(this);
|
||||
this._handleClick = this._handleClick.bind(this);
|
||||
this.dispatchUserAction = this.dispatchUserAction.bind(this);
|
||||
|
||||
// Saved timeout IDs for scheduled state changes, so they can be cancelled
|
||||
this.stateTransitionTimeoutIDs = [];
|
||||
|
@ -74,6 +78,13 @@ class PageAction {
|
|||
this.urlbar.removeAttribute("cfr-recommendation-state");
|
||||
}
|
||||
|
||||
dispatchUserAction(action) {
|
||||
this._dispatchToASRouter(
|
||||
{type: "USER_ACTION", data: action},
|
||||
{browser: this.window.gBrowser.selectedBrowser}
|
||||
);
|
||||
}
|
||||
|
||||
_expand(delay = 0) {
|
||||
if (!delay) {
|
||||
// Non-delayed state change overrides any scheduled state changes
|
||||
|
@ -144,7 +155,7 @@ class PageAction {
|
|||
const mainAction = {
|
||||
label: primary.label,
|
||||
accessKey: primary.accessKey,
|
||||
callback: () => this._dispatchToASRouter(primary.action)
|
||||
callback: () => this.dispatchUserAction(primary.action)
|
||||
};
|
||||
|
||||
const secondaryActions = [{
|
||||
|
@ -243,5 +254,6 @@ const CFRPageActions = {
|
|||
RecommendationMap.clear();
|
||||
}
|
||||
};
|
||||
this.CFRPageActions = CFRPageActions;
|
||||
|
||||
const EXPORTED_SYMBOLS = ["CFRPageActions"];
|
||||
|
|
|
@ -364,6 +364,12 @@ this.TelemetryFeed = class TelemetryFeed {
|
|||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a ping for AS router event. The client_id is set to "n/a" by default,
|
||||
* AS router components could change that by including a boolean "includeClientID"
|
||||
* to the payload of the action, impression_id would be set to "n/a" at the same time.
|
||||
* Note that "includeClientID" will not be included in the result ping.
|
||||
*/
|
||||
createASRouterEvent(action) {
|
||||
const ping = {
|
||||
client_id: "n/a",
|
||||
|
@ -371,6 +377,12 @@ this.TelemetryFeed = class TelemetryFeed {
|
|||
locale: Services.locale.getAppLocaleAsLangTag(),
|
||||
impression_id: this._impressionId
|
||||
};
|
||||
if (action.data.includeClientID) {
|
||||
// Ping-centre client will fill in the client_id if it's not provided in the ping
|
||||
delete ping.client_id;
|
||||
delete action.data.includeClientID;
|
||||
ping.impression_id = "n/a";
|
||||
}
|
||||
return Object.assign(ping, action.data);
|
||||
}
|
||||
|
||||
|
|
|
@ -311,17 +311,25 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
return this.show_spocs && this.store.getState().Prefs.values.showSponsored;
|
||||
}
|
||||
|
||||
dispatchSpocDone(target) {
|
||||
const action = {type: at.POCKET_WAITING_FOR_SPOC, data: false};
|
||||
this.store.dispatch(ac.OnlyToOneContent(action, target));
|
||||
}
|
||||
|
||||
maybeAddSpoc(target) {
|
||||
const updateContent = () => {
|
||||
if (!this.shouldShowSpocs()) {
|
||||
this.dispatchSpocDone(target);
|
||||
return false;
|
||||
}
|
||||
if (Math.random() > this.spocsPerNewTabs) {
|
||||
this.dispatchSpocDone(target);
|
||||
return false;
|
||||
}
|
||||
if (!this.spocs || !this.spocs.length) {
|
||||
// We have stories but no spocs so there's nothing to do and this update can be
|
||||
// removed from the queue.
|
||||
this.dispatchSpocDone(target);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -331,6 +339,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
|
||||
if (!spocs.length) {
|
||||
// There's currently no spoc left to display
|
||||
this.dispatchSpocDone(target);
|
||||
return false;
|
||||
}
|
||||
|
||||
|
@ -342,6 +351,7 @@ this.TopStoriesFeed = class TopStoriesFeed {
|
|||
// Send a content update to the target tab
|
||||
const action = {type: at.SECTION_UPDATE, data: Object.assign({rows}, {id: SECTION_ID})};
|
||||
this.store.dispatch(ac.OnlyToOneContent(action, target));
|
||||
this.dispatchSpocDone(target);
|
||||
return false;
|
||||
};
|
||||
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=وسّع القسم
|
|||
section_menu_action_manage_section=أدِر القسم
|
||||
section_menu_action_manage_webext=أدِر الامتداد
|
||||
section_menu_action_add_topsite=أضف موقعًا شائعًا
|
||||
section_menu_action_add_search_engine=أضِف محرك بحث
|
||||
section_menu_action_move_up=انقل لأعلى
|
||||
section_menu_action_move_down=انقل لأسفل
|
||||
section_menu_action_privacy_notice=تنويه الخصوصية
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=تنويه الخصوصية
|
|||
|
||||
firstrun_continue_to_login=تابِع
|
||||
firstrun_skip_login=تجاوز هذه الخطوة
|
||||
section_menu_action_add_search_engine=أضِف محرك بحث
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Разгъване на раздела
|
|||
section_menu_action_manage_section=Управление на раздела
|
||||
section_menu_action_manage_webext=Управление на добавката
|
||||
section_menu_action_add_topsite=Добавяне на често посещавана страница
|
||||
section_menu_action_add_search_engine=Добавяне на търсеща машина
|
||||
section_menu_action_move_up=Преместване нагоре
|
||||
section_menu_action_move_down=Преместване надолу
|
||||
section_menu_action_privacy_notice=Политика за личните данни
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Политиката за лични данни
|
|||
|
||||
firstrun_continue_to_login=Продължаване
|
||||
firstrun_skip_login=Пропускане
|
||||
section_menu_action_add_search_engine=Добавяне на търсеща машина
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=সেকশনটি প্রসারি
|
|||
section_menu_action_manage_section=সেকশনটি পরিচালনা করুন
|
||||
section_menu_action_manage_webext=এক্সটেনসন ব্যবহার করুন
|
||||
section_menu_action_add_topsite=টপ সাইট যোগ করুন
|
||||
section_menu_action_add_search_engine=অনুসন্ধান ইঞ্জিন যোগ করুন
|
||||
section_menu_action_move_up=উপরে উঠাও
|
||||
section_menu_action_move_down=নীচে নামাও
|
||||
section_menu_action_privacy_notice=গোপনীয়তা নীতি
|
||||
|
@ -180,19 +181,24 @@ section_menu_action_privacy_notice=গোপনীয়তা নীতি
|
|||
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
|
||||
# firstrun of the browser, they give an introduction to Firefox and Sync.
|
||||
firstrun_title=অাপনি Firefox ব্যবহার করুন
|
||||
firstrun_content=আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।
|
||||
firstrun_learn_more_link=Firefox অ্যাকাউন্ট সম্পর্কে আরও জানুন
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
# firstrun_form_header is displayed more boldly as the call to action.
|
||||
firstrun_form_header=আপনার ই-মেইল লিখুন
|
||||
firstrun_form_sub_header=Firefox সিঙ্ক চালিয়ে যেতে
|
||||
|
||||
firstrun_email_input_placeholder=ইমেইল
|
||||
|
||||
firstrun_invalid_input=কার্যকর ইমেইল আবশ্যক
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।
|
||||
firstrun_terms_of_service=সেবার শর্ত
|
||||
firstrun_privacy_notice=গোপনীয়তা নীতি
|
||||
|
||||
firstrun_continue_to_login=চালিয়ে যান
|
||||
firstrun_skip_login=এই ধাপটি বাদ দিন
|
||||
section_menu_action_add_search_engine=অনুসন্ধান ইঞ্জিন যোগ
|
||||
|
|
|
@ -60,7 +60,7 @@ menu_action_open_file=Obre el fitxer
|
|||
# link that belongs to this downloaded item"
|
||||
menu_action_copy_download_link=Copia l'enllaç de la baixada
|
||||
menu_action_go_to_download_page=Vés a la pàgina de la baixada
|
||||
menu_action_remove_download=Suprimeix de l'historial
|
||||
menu_action_remove_download=Elimina de l'historial
|
||||
|
||||
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
|
||||
# search button.
|
||||
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Amplia la secció
|
|||
section_menu_action_manage_section=Gestiona la secció
|
||||
section_menu_action_manage_webext=Gestiona l'extensió
|
||||
section_menu_action_add_topsite=Afegeix com a lloc principal
|
||||
section_menu_action_add_search_engine=Afegeix un motor de cerca
|
||||
section_menu_action_move_up=Mou cap amunt
|
||||
section_menu_action_move_down=Mou cap avall
|
||||
section_menu_action_privacy_notice=Avís de privadesa
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Avís de privadesa
|
|||
|
||||
firstrun_continue_to_login=Continua
|
||||
firstrun_skip_login=Omet aquest pas
|
||||
section_menu_action_add_search_engine=Afegeix un motor de cerca
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Wótrězk pokazaś
|
|||
section_menu_action_manage_section=Wótrězk zastojaś
|
||||
section_menu_action_manage_webext=Rozšyrjenje zastojaś
|
||||
section_menu_action_add_topsite=Woblubowane sedło pśidaś
|
||||
section_menu_action_add_search_engine=Pytnicu pśidaś
|
||||
section_menu_action_move_up=Górjej
|
||||
section_menu_action_move_down=Dołoj
|
||||
section_menu_action_privacy_notice=Powěźeńka priwatnosći
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Powěźeńka priwatnosći
|
|||
|
||||
firstrun_continue_to_login=Dalej
|
||||
firstrun_skip_login=Toś ten kšac pśeskócyś
|
||||
section_menu_action_add_search_engine=Pytnicu pśidaś
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Malfaldi sekcion
|
|||
section_menu_action_manage_section=Administri sekcion
|
||||
section_menu_action_manage_webext=Administri etendaĵon
|
||||
section_menu_action_add_topsite=Aldoni oftan retejon
|
||||
section_menu_action_add_search_engine=Aldoni serĉilon
|
||||
section_menu_action_move_up=Movi supren
|
||||
section_menu_action_move_down=Movi malsupren
|
||||
section_menu_action_privacy_notice=Rimarko pri privateco
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=rimarkon pri privateco
|
|||
|
||||
firstrun_continue_to_login=Daŭrigi
|
||||
firstrun_skip_login=Pretersalti tiun ĉi paŝon
|
||||
section_menu_action_add_search_engine=Aldoni serĉilon
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Expandir sección
|
|||
section_menu_action_manage_section=Gestionar sección
|
||||
section_menu_action_manage_webext=Gestionar extensión
|
||||
section_menu_action_add_topsite=Añadir sitio popular
|
||||
section_menu_action_add_search_engine=Añadir motor de búsqueda
|
||||
section_menu_action_move_up=Subir
|
||||
section_menu_action_move_down=Bajar
|
||||
section_menu_action_privacy_notice=Aviso de privacidad
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Aviso de privacidad
|
|||
|
||||
firstrun_continue_to_login=Continuar
|
||||
firstrun_skip_login=Saltar este paso
|
||||
section_menu_action_add_search_engine=Añadir motor de búsqueda
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Ampliar la sección
|
|||
section_menu_action_manage_section=Administrar sección
|
||||
section_menu_action_manage_webext=Gestionar extensión
|
||||
section_menu_action_add_topsite=Agregar sitio popular
|
||||
section_menu_action_add_search_engine=Agregar motor de búsqueda
|
||||
section_menu_action_move_up=Subir
|
||||
section_menu_action_move_down=Bajar
|
||||
section_menu_action_privacy_notice=Política de privacidad
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Política de privacidad
|
|||
|
||||
firstrun_continue_to_login=Continuar
|
||||
firstrun_skip_login=Saltar este paso
|
||||
section_menu_action_add_search_engine=Agregar motor de búsqueda
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Zabaldu atala
|
|||
section_menu_action_manage_section=Kudeatu atala
|
||||
section_menu_action_manage_webext=Kudeatu hedapena
|
||||
section_menu_action_add_topsite=Gehitu maiz erabilitako gunea
|
||||
section_menu_action_add_search_engine=Gehitu bilaketa-motorra
|
||||
section_menu_action_move_up=Eraman gora
|
||||
section_menu_action_move_down=Eraman behera
|
||||
section_menu_action_privacy_notice=Pribatutasun-oharra
|
||||
|
@ -191,6 +192,8 @@ firstrun_form_sub_header=Firefox Sync-ekin jarraitzeko.
|
|||
|
||||
firstrun_email_input_placeholder=Helbide elektronikoa
|
||||
|
||||
firstrun_invalid_input=Baliozko helbide elektronikoa behar da
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=Jarraitzearekin bat, {terms} eta {privacy} onartzen dituzu.
|
||||
|
@ -199,4 +202,3 @@ firstrun_privacy_notice=Pribatutasun-oharra
|
|||
|
||||
firstrun_continue_to_login=Jarraitu
|
||||
firstrun_skip_login=Saltatu urrats hau
|
||||
section_menu_action_add_search_engine=Gehitu bilaketa-motorra
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=વિભાગ વિસ્તૃત કર
|
|||
section_menu_action_manage_section=વિભાગ સંચાલિત કરો
|
||||
section_menu_action_manage_webext=એક્સ્ટેંશનનો વહીવટ કરો
|
||||
section_menu_action_add_topsite=ટોચની સાઇટ ઉમેરો
|
||||
section_menu_action_add_search_engine=શોધ એંજીન ઉમેરો
|
||||
section_menu_action_move_up=ઉપર કરો
|
||||
section_menu_action_move_down=નીચે કરો
|
||||
section_menu_action_privacy_notice=ખાનગી સૂચના
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=ખાનગી સૂચના
|
|||
|
||||
firstrun_continue_to_login=ચાલુ રાખો
|
||||
firstrun_skip_login=આ પગલું છોડી દો
|
||||
section_menu_action_add_search_engine=શોધ યંત્ર ઉમેરો
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=अनुभाग विस्तृत क
|
|||
section_menu_action_manage_section=अनुभाग प्रबंधित करें
|
||||
section_menu_action_manage_webext=विस्तारक प्रबंधित करें
|
||||
section_menu_action_add_topsite=शीर्ष साइट जोड़ें
|
||||
section_menu_action_add_search_engine=खोज ईंजन जोड़ें
|
||||
section_menu_action_move_up=ऊपर जाएँ
|
||||
section_menu_action_move_down=नीचे जाएँ
|
||||
section_menu_action_privacy_notice=गोपनीयता नीति
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=गोपनीयता नीति
|
|||
|
||||
firstrun_continue_to_login=जारी रखें
|
||||
firstrun_skip_login=इस चरण को छोड़ दें
|
||||
section_menu_action_add_search_engine=सर्च इंजन जोड़े
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Wotrězk pokazać
|
|||
section_menu_action_manage_section=Wotrězk rjadować
|
||||
section_menu_action_manage_webext=Rozšěrjenje rjadować
|
||||
section_menu_action_add_topsite=Woblubowane sydło přidać
|
||||
section_menu_action_add_search_engine=Pytawu přidać
|
||||
section_menu_action_move_up=Horje
|
||||
section_menu_action_move_down=Dele
|
||||
section_menu_action_privacy_notice=Zdźělenka priwatnosće
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Zdźělenka priwatnosće
|
|||
|
||||
firstrun_continue_to_login=Pokročować
|
||||
firstrun_skip_login=Tutón krok přeskočić
|
||||
section_menu_action_add_search_engine=Pytawu přidać
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Expander le section
|
|||
section_menu_action_manage_section=Gerer le section
|
||||
section_menu_action_manage_webext=Gerer extension
|
||||
section_menu_action_add_topsite=Adder a sito popular
|
||||
section_menu_action_add_search_engine=Adder un motor de recerca
|
||||
section_menu_action_move_up=Mover in alto
|
||||
section_menu_action_move_down=Mover in basso
|
||||
section_menu_action_privacy_notice=Notification de confidentialitate
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Notification de confidentialitate
|
|||
|
||||
firstrun_continue_to_login=Continuar
|
||||
firstrun_skip_login=Saltar iste grado
|
||||
section_menu_action_add_search_engine=Adder un motor de recerca
|
||||
|
|
|
@ -125,9 +125,9 @@ topsites_form_add_header=ახალი საიტი რჩეულებ
|
|||
topsites_form_edit_header=რჩეული საიტის ჩასწორება
|
||||
topsites_form_title_label=დასახელება
|
||||
topsites_form_title_placeholder=სათაურის შეყვანა
|
||||
topsites_form_url_label=URL ბმული
|
||||
topsites_form_image_url_label=სასურველი სურათის URL ბმული
|
||||
topsites_form_url_placeholder=აკრიფეთ ან ჩასვით URL ბმული
|
||||
topsites_form_url_label=URL-ბმული
|
||||
topsites_form_image_url_label=სასურველი სურათის URL-ბმული
|
||||
topsites_form_url_placeholder=აკრიფეთ ან ჩასვით URL-ბმული
|
||||
topsites_form_use_image_link=სასურველი სურათის გამოყენება…
|
||||
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
|
||||
topsites_form_preview_button=შეთვალიერება
|
||||
|
@ -135,7 +135,7 @@ topsites_form_add_button=დამატება
|
|||
topsites_form_save_button=შენახვა
|
||||
topsites_form_cancel_button=გაუქმება
|
||||
topsites_form_url_validation=საჭიროა მართებული URL
|
||||
topsites_form_image_validation=სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL ბმული.
|
||||
topsites_form_image_validation=სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL-ბმული.
|
||||
|
||||
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
|
||||
# trending stories section and precedes a list of links to popular topics.
|
||||
|
@ -182,7 +182,7 @@ section_menu_action_privacy_notice=პირადი მონაცემე
|
|||
# firstrun of the browser, they give an introduction to Firefox and Sync.
|
||||
firstrun_title=თან წაიყოლეთ Firefox
|
||||
firstrun_content=მიიღეთ წვდომა თქვენს სანიშნებთან, ისტორიასთან, პაროლებსა და სხვა პარამეტრებთან, ყველა თქვენს მოწყობილობაზე.
|
||||
firstrun_learn_more_link=იხილეთ ვრცლად, Firefox ანგარიშების შესახებ
|
||||
firstrun_learn_more_link=იხილეთ ვრცლად, Firefox-ანგარიშების შესახებ
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Snefli tigezmi
|
|||
section_menu_action_manage_section=Sefrek tigezmi
|
||||
section_menu_action_manage_webext=Sefrek asiɣzef
|
||||
section_menu_action_add_topsite=Rnu asmel ifazen
|
||||
section_menu_action_add_search_engine=Rnu amsedday n unadi
|
||||
section_menu_action_move_up=Ali
|
||||
section_menu_action_move_down=Ader
|
||||
section_menu_action_privacy_notice=Tasertit n tbaḍnit
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=Tasertit n tbaḍnit
|
|||
|
||||
firstrun_continue_to_login=Kemmel
|
||||
firstrun_skip_login=Zgel amecwaṛ-agi
|
||||
section_menu_action_add_search_engine=Rnu amsedday n unadi
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=섹션 열기
|
|||
section_menu_action_manage_section=섹션 관리
|
||||
section_menu_action_manage_webext=부가 기능 관리
|
||||
section_menu_action_add_topsite=인기 사이트 추가
|
||||
section_menu_action_add_search_engine=검색 엔진 추가
|
||||
section_menu_action_move_up=위로 이동
|
||||
section_menu_action_move_down=아래로 이동
|
||||
section_menu_action_privacy_notice=개인 정보 보호 정책
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=개인 정보 보호 정책
|
|||
|
||||
firstrun_continue_to_login=계속
|
||||
firstrun_skip_login=단계 건너뛰기
|
||||
section_menu_action_add_search_engine=검색 엔진 추가
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Išplėsti skiltį
|
|||
section_menu_action_manage_section=Tvarkyti skiltį
|
||||
section_menu_action_manage_webext=Tvarkyti priedą
|
||||
section_menu_action_add_topsite=Pridėti lankomą svetainę
|
||||
section_menu_action_add_search_engine=Pridėti ieškyklę
|
||||
section_menu_action_move_up=Pakelti
|
||||
section_menu_action_move_down=Nuleisti
|
||||
section_menu_action_privacy_notice=Privatumo nuostatai
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=privatumo nuostatais
|
|||
|
||||
firstrun_continue_to_login=Tęsti
|
||||
firstrun_skip_login=Praleisti šį žingsnį
|
||||
section_menu_action_add_search_engine=Pridėti ieškyklę
|
||||
|
|
|
@ -50,6 +50,9 @@ menu_action_archive_pocket=Pocket मध्ये संग्रहित क
|
|||
# "this action" is that it will show where the downloaded file exists on the file system
|
||||
# for each operating system.
|
||||
menu_action_show_file_mac_os=Finder मध्ये दर्शवा
|
||||
menu_action_show_file_windows=समाविष्ट करणारे फोल्डर उघडा
|
||||
menu_action_show_file_linux=समाविष्ट करणारे फोल्डर उघडा
|
||||
menu_action_show_file_default=फाईल दाखवा
|
||||
menu_action_open_file=फाइल उघडा
|
||||
|
||||
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
|
||||
|
@ -95,9 +98,12 @@ prefs_section_rows_option={num} ओळ;{num} ओळी
|
|||
prefs_search_header=वेब शोध
|
||||
prefs_topsites_description=आपण सर्वाधिक भेट देता त्या साइट
|
||||
prefs_topstories_description2=आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री
|
||||
prefs_topstories_options_sponsored_label=प्रायोजित कथा
|
||||
prefs_topstories_sponsored_learn_more=अधिक जाणून घ्या
|
||||
prefs_highlights_description=आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा
|
||||
prefs_highlights_options_visited_label=भेट दिलेली पृष्ठे
|
||||
prefs_highlights_options_download_label=अलीकडचे डाउनलोड
|
||||
prefs_highlights_options_pocket_label=Pocket मध्ये जतन केलेले पृष्ठ
|
||||
prefs_snippets_description=Mozilla आणि Firefox कडून अद्यतने
|
||||
settings_pane_button_label=आपले नवीन टॅब पृष्ठ सानुकूलित करा
|
||||
settings_pane_topsites_header=शीर्ष साइट्स
|
||||
|
@ -120,13 +126,16 @@ topsites_form_edit_header=खास साईट संपादित करा
|
|||
topsites_form_title_label=शिर्षक
|
||||
topsites_form_title_placeholder=शिर्षक प्रविष्ट करा
|
||||
topsites_form_url_label=URL
|
||||
topsites_form_image_url_label=सानुकूल प्रतिमा URL
|
||||
topsites_form_url_placeholder=URL चिकटवा किंवा टाईप करा
|
||||
topsites_form_use_image_link=सानुकूल प्रतिमा वापरा…
|
||||
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
|
||||
topsites_form_preview_button=पूर्वावलोकन
|
||||
topsites_form_add_button=समाविष्ट करा
|
||||
topsites_form_save_button=जतन करा
|
||||
topsites_form_cancel_button=रद्द करा
|
||||
topsites_form_url_validation=वैध URL आवश्यक
|
||||
topsites_form_image_validation=प्रतिमा लोड झाली नाही. वेगळी URL वापरून पहा.
|
||||
|
||||
# LOCALIZATION NOTE (pocket_read_more): This is shown at the bottom of the
|
||||
# trending stories section and precedes a list of links to popular topics.
|
||||
|
@ -153,12 +162,18 @@ manual_migration_import_button=आता आयात करा
|
|||
|
||||
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
|
||||
# action link are shown in each section of UI that fails to render
|
||||
error_fallback_default_info=अरेरे, हा मजकूर लोड करताना काहीतरी गोंधळ झाला.
|
||||
error_fallback_default_refresh_suggestion=पुन्हा प्रयत्न करण्यासाठी पृष्ठ रिफ्रेश करा.
|
||||
|
||||
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
|
||||
# context menu and are meant as a call to action for the given section.
|
||||
section_menu_action_remove_section=विभाग काढा
|
||||
section_menu_action_collapse_section=विभाग ढासळा
|
||||
section_menu_action_expand_section=विभाग वाढवा
|
||||
section_menu_action_manage_section=विभाग व्यवस्थापित करा
|
||||
section_menu_action_manage_webext=एक्सटेन्शन व्यवस्थापित करा
|
||||
section_menu_action_add_topsite=खास साईट्स जोडा
|
||||
section_menu_action_add_search_engine=शोध इंजीन जोडा
|
||||
section_menu_action_move_up=वर जा
|
||||
section_menu_action_move_down=खाली जा
|
||||
section_menu_action_privacy_notice=गोपनीयता सूचना
|
||||
|
@ -166,21 +181,24 @@ section_menu_action_privacy_notice=गोपनीयता सूचना
|
|||
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
|
||||
# firstrun of the browser, they give an introduction to Firefox and Sync.
|
||||
firstrun_title=Firefox सोबत न्या
|
||||
firstrun_content=आपले बुकमार्क्स, इतिहास, पासवर्ड आणि इतर सेटिंग आपल्या सर्व उपकरणांवर मिळवा.
|
||||
firstrun_learn_more_link=Firefox खात्यांविषयी अधिक जाणून घ्या
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
|
||||
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
|
||||
# firstrun_form_header is displayed more boldly as the call to action.
|
||||
firstrun_form_header=ईमेल प्रविष्ट करा
|
||||
firstrun_form_sub_header=Firefox Sync वर सुरू ठेवण्यासाठी
|
||||
|
||||
firstrun_email_input_placeholder=ईमेल
|
||||
|
||||
firstrun_invalid_input=वैध ईमेल आवश्यक
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_extra_legal_links=पुढे जाताना आपण {terms} आणि {privacy} यांना संमती देता.
|
||||
firstrun_terms_of_service=सेवा अटी
|
||||
firstrun_privacy_notice=गोपनीयता सूचना
|
||||
|
||||
firstrun_continue_to_login=पुढे चला
|
||||
firstrun_skip_login=ही पायरी वगळा
|
||||
section_menu_action_add_search_engine=शोध इंजीन जोडा
|
||||
|
|
|
@ -172,6 +172,7 @@ section_menu_action_expand_section=Desplegar la seccion
|
|||
section_menu_action_manage_section=Gerir la seccion
|
||||
section_menu_action_manage_webext=Gerir 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
|
||||
|
@ -200,4 +201,3 @@ firstrun_privacy_notice=Avís de privacitat
|
|||
|
||||
firstrun_continue_to_login=Contunhar
|
||||
firstrun_skip_login=Passar aquesta etapa
|
||||
section_menu_action_add_search_engine=Apondre un motor de recèrca
|
||||
|
|
|
@ -99,6 +99,7 @@ section_menu_action_expand_section=Rozwiń sekcję
|
|||
section_menu_action_manage_section=Zarządzaj sekcją
|
||||
section_menu_action_manage_webext=Zarządzaj rozszerzeniem
|
||||
section_menu_action_add_topsite=Dodaj stronę do popularnych
|
||||
section_menu_action_add_search_engine=Dodaj wyszukiwarkę
|
||||
section_menu_action_move_up=Przesuń w górę
|
||||
section_menu_action_move_down=Przesuń w dół
|
||||
section_menu_action_privacy_notice=Uwagi dotyczące prywatności
|
||||
|
@ -115,4 +116,3 @@ firstrun_terms_of_service=warunki korzystania z usługi
|
|||
firstrun_privacy_notice=uwagi dotyczące prywatności
|
||||
firstrun_continue_to_login=Kontynuuj
|
||||
firstrun_skip_login=Pomiń
|
||||
section_menu_action_add_search_engine=Dodaj wyszukiwarkę
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=Развернуть раздел
|
|||
section_menu_action_manage_section=Управление разделом
|
||||
section_menu_action_manage_webext=Управление расширением
|
||||
section_menu_action_add_topsite=Добавить в топ сайтов
|
||||
section_menu_action_add_search_engine=Добавить поисковую систему
|
||||
section_menu_action_move_up=Вверх
|
||||
section_menu_action_move_down=Вниз
|
||||
section_menu_action_privacy_notice=Уведомление о приватности
|
||||
|
@ -201,4 +202,3 @@ firstrun_privacy_notice=политикой приватности
|
|||
|
||||
firstrun_continue_to_login=Продолжить
|
||||
firstrun_skip_login=Пропустить этот шаг
|
||||
section_menu_action_add_search_engine=Добавить поисковую систему
|
||||
|
|
|
@ -192,6 +192,7 @@ firstrun_form_sub_header=za nadaljevanje v Firefox Sync.
|
|||
|
||||
firstrun_email_input_placeholder=E-pošta
|
||||
|
||||
firstrun_invalid_input=Zahtevan je veljaven e-poštni naslov
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
|
|
|
@ -173,6 +173,7 @@ section_menu_action_expand_section=ขยายส่วน
|
|||
section_menu_action_manage_section=จัดการส่วน
|
||||
section_menu_action_manage_webext=จัดการส่วนขยาย
|
||||
section_menu_action_add_topsite=เพิ่มไซต์เด่น
|
||||
section_menu_action_add_search_engine=เพิ่มเครื่องมือค้นหา
|
||||
section_menu_action_move_up=ย้ายขึ้น
|
||||
section_menu_action_move_down=ย้ายลง
|
||||
section_menu_action_privacy_notice=ประกาศความเป็นส่วนตัว
|
||||
|
@ -191,6 +192,7 @@ firstrun_form_sub_header=เพื่อดำเนินการต่อไ
|
|||
|
||||
firstrun_email_input_placeholder=อีเมล
|
||||
|
||||
|
||||
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
|
||||
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
|
||||
firstrun_terms_of_service=เงื่อนไขการให้บริการ
|
||||
|
@ -198,4 +200,3 @@ firstrun_privacy_notice=ประกาศความเป็นส่วน
|
|||
|
||||
firstrun_continue_to_login=ดำเนินการต่อ
|
||||
firstrun_skip_login=ข้ามขั้นตอนนี้
|
||||
section_menu_action_add_search_engine=เพิ่มเครื่องมือค้นหา
|
||||
|
|
|
@ -87,18 +87,18 @@ window.gActivityStreamStrings = {
|
|||
"section_menu_action_manage_section": "সেকশনটি পরিচালনা করুন",
|
||||
"section_menu_action_manage_webext": "এক্সটেনসন ব্যবহার করুন",
|
||||
"section_menu_action_add_topsite": "টপ সাইট যোগ করুন",
|
||||
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ",
|
||||
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ করুন",
|
||||
"section_menu_action_move_up": "উপরে উঠাও",
|
||||
"section_menu_action_move_down": "নীচে নামাও",
|
||||
"section_menu_action_privacy_notice": "গোপনীয়তা নীতি",
|
||||
"firstrun_title": "অাপনি Firefox ব্যবহার করুন",
|
||||
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
|
||||
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
|
||||
"firstrun_content": "আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।",
|
||||
"firstrun_learn_more_link": "Firefox অ্যাকাউন্ট সম্পর্কে আরও জানুন",
|
||||
"firstrun_form_header": "আপনার ই-মেইল লিখুন",
|
||||
"firstrun_form_sub_header": "to continue to Firefox Sync",
|
||||
"firstrun_form_sub_header": "Firefox সিঙ্ক চালিয়ে যেতে",
|
||||
"firstrun_email_input_placeholder": "ইমেইল",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
|
||||
"firstrun_invalid_input": "কার্যকর ইমেইল আবশ্যক",
|
||||
"firstrun_extra_legal_links": "অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।",
|
||||
"firstrun_terms_of_service": "সেবার শর্ত",
|
||||
"firstrun_privacy_notice": "গোপনীয়তা নীতি",
|
||||
"firstrun_continue_to_login": "চালিয়ে যান",
|
||||
|
|
|
@ -87,18 +87,18 @@ window.gActivityStreamStrings = {
|
|||
"section_menu_action_manage_section": "সেকশনটি পরিচালনা করুন",
|
||||
"section_menu_action_manage_webext": "এক্সটেনসন ব্যবহার করুন",
|
||||
"section_menu_action_add_topsite": "টপ সাইট যোগ করুন",
|
||||
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ",
|
||||
"section_menu_action_add_search_engine": "অনুসন্ধান ইঞ্জিন যোগ করুন",
|
||||
"section_menu_action_move_up": "উপরে উঠাও",
|
||||
"section_menu_action_move_down": "নীচে নামাও",
|
||||
"section_menu_action_privacy_notice": "গোপনীয়তা নীতি",
|
||||
"firstrun_title": "অাপনি Firefox ব্যবহার করুন",
|
||||
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
|
||||
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
|
||||
"firstrun_content": "আপনার সমস্ত ডিভাইসে আপনার বুকমার্ক, ইতিহাস, পাসওয়ার্ড এবং অন্যান্য সেটিংস পাওয়া যাবে।",
|
||||
"firstrun_learn_more_link": "Firefox অ্যাকাউন্ট সম্পর্কে আরও জানুন",
|
||||
"firstrun_form_header": "আপনার ই-মেইল লিখুন",
|
||||
"firstrun_form_sub_header": "to continue to Firefox Sync",
|
||||
"firstrun_form_sub_header": "Firefox সিঙ্ক চালিয়ে যেতে",
|
||||
"firstrun_email_input_placeholder": "ইমেইল",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
|
||||
"firstrun_invalid_input": "কার্যকর ইমেইল আবশ্যক",
|
||||
"firstrun_extra_legal_links": "অগ্রসর হওয়ার মাধ্যমে আপনি {terms} এবং {privacy} এর সাথে সম্মত হচ্ছেন।",
|
||||
"firstrun_terms_of_service": "সেবার শর্ত",
|
||||
"firstrun_privacy_notice": "গোপনীয়তা নীতি",
|
||||
"firstrun_continue_to_login": "চালিয়ে যান",
|
||||
|
|
|
@ -31,7 +31,7 @@ window.gActivityStreamStrings = {
|
|||
"menu_action_open_file": "Obre el fitxer",
|
||||
"menu_action_copy_download_link": "Copia l'enllaç de la baixada",
|
||||
"menu_action_go_to_download_page": "Vés a la pàgina de la baixada",
|
||||
"menu_action_remove_download": "Suprimeix de l'historial",
|
||||
"menu_action_remove_download": "Elimina de l'historial",
|
||||
"search_button": "Cerca",
|
||||
"search_header": "Cerca de {search_engine_name}",
|
||||
"search_web_placeholder": "Cerca al web",
|
||||
|
|
|
@ -97,7 +97,7 @@ window.gActivityStreamStrings = {
|
|||
"firstrun_form_header": "Idatzi zure helbide elektronikoa",
|
||||
"firstrun_form_sub_header": "Firefox Sync-ekin jarraitzeko.",
|
||||
"firstrun_email_input_placeholder": "Helbide elektronikoa",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "Baliozko helbide elektronikoa behar da",
|
||||
"firstrun_extra_legal_links": "Jarraitzearekin bat, {terms} eta {privacy} onartzen dituzu.",
|
||||
"firstrun_terms_of_service": "Zerbitzu-baldintzak",
|
||||
"firstrun_privacy_notice": "Pribatutasun-oharra",
|
||||
|
|
|
@ -87,7 +87,7 @@ window.gActivityStreamStrings = {
|
|||
"section_menu_action_manage_section": "વિભાગ સંચાલિત કરો",
|
||||
"section_menu_action_manage_webext": "એક્સ્ટેંશનનો વહીવટ કરો",
|
||||
"section_menu_action_add_topsite": "ટોચની સાઇટ ઉમેરો",
|
||||
"section_menu_action_add_search_engine": "શોધ યંત્ર ઉમેરો",
|
||||
"section_menu_action_add_search_engine": "શોધ એંજીન ઉમેરો",
|
||||
"section_menu_action_move_up": "ઉપર કરો",
|
||||
"section_menu_action_move_down": "નીચે કરો",
|
||||
"section_menu_action_privacy_notice": "ખાનગી સૂચના",
|
||||
|
|
|
@ -87,7 +87,7 @@ window.gActivityStreamStrings = {
|
|||
"section_menu_action_manage_section": "अनुभाग प्रबंधित करें",
|
||||
"section_menu_action_manage_webext": "विस्तारक प्रबंधित करें",
|
||||
"section_menu_action_add_topsite": "शीर्ष साइट जोड़ें",
|
||||
"section_menu_action_add_search_engine": "सर्च इंजन जोड़े",
|
||||
"section_menu_action_add_search_engine": "खोज ईंजन जोड़ें",
|
||||
"section_menu_action_move_up": "ऊपर जाएँ",
|
||||
"section_menu_action_move_down": "नीचे जाएँ",
|
||||
"section_menu_action_privacy_notice": "गोपनीयता नीति",
|
||||
|
|
|
@ -62,16 +62,16 @@ window.gActivityStreamStrings = {
|
|||
"topsites_form_edit_header": "რჩეული საიტის ჩასწორება",
|
||||
"topsites_form_title_label": "დასახელება",
|
||||
"topsites_form_title_placeholder": "სათაურის შეყვანა",
|
||||
"topsites_form_url_label": "URL ბმული",
|
||||
"topsites_form_image_url_label": "სასურველი სურათის URL ბმული",
|
||||
"topsites_form_url_placeholder": "აკრიფეთ ან ჩასვით URL ბმული",
|
||||
"topsites_form_url_label": "URL-ბმული",
|
||||
"topsites_form_image_url_label": "სასურველი სურათის URL-ბმული",
|
||||
"topsites_form_url_placeholder": "აკრიფეთ ან ჩასვით URL-ბმული",
|
||||
"topsites_form_use_image_link": "სასურველი სურათის გამოყენება…",
|
||||
"topsites_form_preview_button": "შეთვალიერება",
|
||||
"topsites_form_add_button": "დამატება",
|
||||
"topsites_form_save_button": "შენახვა",
|
||||
"topsites_form_cancel_button": "გაუქმება",
|
||||
"topsites_form_url_validation": "საჭიროა მართებული URL",
|
||||
"topsites_form_image_validation": "სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL ბმული.",
|
||||
"topsites_form_image_validation": "სურათი ვერ ჩაიტვირთა. სცადეთ სხვა URL-ბმული.",
|
||||
"pocket_read_more": "პოპულარული თემები:",
|
||||
"pocket_read_even_more": "მეტი სიახლის ნახვა",
|
||||
"highlights_empty_state": "დაიწყეთ გვერდების დათვალიერება და აქ გამოჩნდება თქვენთვის სასურველი სტატიები, ვიდეოები და ბოლოს მონახულებული ან ჩანიშნული საიტები.",
|
||||
|
@ -93,7 +93,7 @@ window.gActivityStreamStrings = {
|
|||
"section_menu_action_privacy_notice": "პირადი მონაცემების დაცვის განაცხადი",
|
||||
"firstrun_title": "თან წაიყოლეთ Firefox",
|
||||
"firstrun_content": "მიიღეთ წვდომა თქვენს სანიშნებთან, ისტორიასთან, პაროლებსა და სხვა პარამეტრებთან, ყველა თქვენს მოწყობილობაზე.",
|
||||
"firstrun_learn_more_link": "იხილეთ ვრცლად, Firefox ანგარიშების შესახებ",
|
||||
"firstrun_learn_more_link": "იხილეთ ვრცლად, Firefox-ანგარიშების შესახებ",
|
||||
"firstrun_form_header": "შეიყვანეთ თქვენი ელფოსტა",
|
||||
"firstrun_form_sub_header": "Firefox Sync-ზე გადასასვლელად.",
|
||||
"firstrun_email_input_placeholder": "ელფოსტა",
|
||||
|
|
|
@ -25,9 +25,9 @@ window.gActivityStreamStrings = {
|
|||
"menu_action_delete_pocket": "Pocket मधून हटवा",
|
||||
"menu_action_archive_pocket": "Pocket मध्ये संग्रहित करा",
|
||||
"menu_action_show_file_mac_os": "Finder मध्ये दर्शवा",
|
||||
"menu_action_show_file_windows": "Open Containing Folder",
|
||||
"menu_action_show_file_linux": "Open Containing Folder",
|
||||
"menu_action_show_file_default": "Show File",
|
||||
"menu_action_show_file_windows": "समाविष्ट करणारे फोल्डर उघडा",
|
||||
"menu_action_show_file_linux": "समाविष्ट करणारे फोल्डर उघडा",
|
||||
"menu_action_show_file_default": "फाईल दाखवा",
|
||||
"menu_action_open_file": "फाइल उघडा",
|
||||
"menu_action_copy_download_link": "डाउनलोड दुव्याची प्रत बनवा",
|
||||
"menu_action_go_to_download_page": "डाउनलोड पृष्ठावर जा",
|
||||
|
@ -44,12 +44,12 @@ window.gActivityStreamStrings = {
|
|||
"prefs_search_header": "वेब शोध",
|
||||
"prefs_topsites_description": "आपण सर्वाधिक भेट देता त्या साइट",
|
||||
"prefs_topstories_description2": "आपल्यासाठी वैयक्तिकीकृत केलेल्या वेबवरील छान सामग्री",
|
||||
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
|
||||
"prefs_topstories_options_sponsored_label": "प्रायोजित कथा",
|
||||
"prefs_topstories_sponsored_learn_more": "अधिक जाणून घ्या",
|
||||
"prefs_highlights_description": "आपण जतन केलेल्या किंवा भेट दिलेल्या साइट्सचा एक निवडक साठा",
|
||||
"prefs_highlights_options_visited_label": "भेट दिलेली पृष्ठे",
|
||||
"prefs_highlights_options_download_label": "Most Recent Download",
|
||||
"prefs_highlights_options_pocket_label": "Pages Saved to Pocket",
|
||||
"prefs_highlights_options_download_label": "अलीकडचे डाउनलोड",
|
||||
"prefs_highlights_options_pocket_label": "Pocket मध्ये जतन केलेले पृष्ठ",
|
||||
"prefs_snippets_description": "Mozilla आणि Firefox कडून अद्यतने",
|
||||
"settings_pane_button_label": "आपले नवीन टॅब पृष्ठ सानुकूलित करा",
|
||||
"settings_pane_topsites_header": "शीर्ष साइट्स",
|
||||
|
@ -63,15 +63,15 @@ window.gActivityStreamStrings = {
|
|||
"topsites_form_title_label": "शिर्षक",
|
||||
"topsites_form_title_placeholder": "शिर्षक प्रविष्ट करा",
|
||||
"topsites_form_url_label": "URL",
|
||||
"topsites_form_image_url_label": "Custom Image URL",
|
||||
"topsites_form_image_url_label": "सानुकूल प्रतिमा URL",
|
||||
"topsites_form_url_placeholder": "URL चिकटवा किंवा टाईप करा",
|
||||
"topsites_form_use_image_link": "Use a custom image…",
|
||||
"topsites_form_use_image_link": "सानुकूल प्रतिमा वापरा…",
|
||||
"topsites_form_preview_button": "पूर्वावलोकन",
|
||||
"topsites_form_add_button": "समाविष्ट करा",
|
||||
"topsites_form_save_button": "जतन करा",
|
||||
"topsites_form_cancel_button": "रद्द करा",
|
||||
"topsites_form_url_validation": "वैध URL आवश्यक",
|
||||
"topsites_form_image_validation": "Image failed to load. Try a different URL.",
|
||||
"topsites_form_image_validation": "प्रतिमा लोड झाली नाही. वेगळी URL वापरून पहा.",
|
||||
"pocket_read_more": "लोकप्रिय विषय:",
|
||||
"pocket_read_even_more": "अधिक कथा पहा",
|
||||
"highlights_empty_state": "ब्राउझिंग सुरू करा, आणि आम्ही आपल्याला इथे आपण अलीकडील भेट दिलेले किंवा वाचनखूण लावलेले उत्कृष्ठ लेख, व्हिडिओ, आणि इतर पृष्ठांपैकी काही दाखवू.",
|
||||
|
@ -79,26 +79,26 @@ window.gActivityStreamStrings = {
|
|||
"manual_migration_explanation2": "दुसऱ्या ब्राऊझरमधील वाचनखूणा, इतिहास आणि पासवर्ड सोबत Firefox ला वापरून पहा.",
|
||||
"manual_migration_cancel_button": "नाही धन्यवाद",
|
||||
"manual_migration_import_button": "आता आयात करा",
|
||||
"error_fallback_default_info": "Oops, something went wrong loading this content.",
|
||||
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
|
||||
"error_fallback_default_info": "अरेरे, हा मजकूर लोड करताना काहीतरी गोंधळ झाला.",
|
||||
"error_fallback_default_refresh_suggestion": "पुन्हा प्रयत्न करण्यासाठी पृष्ठ रिफ्रेश करा.",
|
||||
"section_menu_action_remove_section": "विभाग काढा",
|
||||
"section_menu_action_collapse_section": "विभाग ढासळा",
|
||||
"section_menu_action_expand_section": "Expand Section",
|
||||
"section_menu_action_manage_section": "Manage Section",
|
||||
"section_menu_action_expand_section": "विभाग वाढवा",
|
||||
"section_menu_action_manage_section": "विभाग व्यवस्थापित करा",
|
||||
"section_menu_action_manage_webext": "एक्सटेन्शन व्यवस्थापित करा",
|
||||
"section_menu_action_add_topsite": "Add Top Site",
|
||||
"section_menu_action_add_topsite": "खास साईट्स जोडा",
|
||||
"section_menu_action_add_search_engine": "शोध इंजीन जोडा",
|
||||
"section_menu_action_move_up": "वर जा",
|
||||
"section_menu_action_move_down": "खाली जा",
|
||||
"section_menu_action_privacy_notice": "गोपनीयता सूचना",
|
||||
"firstrun_title": "Firefox सोबत न्या",
|
||||
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
|
||||
"firstrun_content": "आपले बुकमार्क्स, इतिहास, पासवर्ड आणि इतर सेटिंग आपल्या सर्व उपकरणांवर मिळवा.",
|
||||
"firstrun_learn_more_link": "Firefox खात्यांविषयी अधिक जाणून घ्या",
|
||||
"firstrun_form_header": "ईमेल प्रविष्ट करा",
|
||||
"firstrun_form_sub_header": "to continue to Firefox Sync",
|
||||
"firstrun_form_sub_header": "Firefox Sync वर सुरू ठेवण्यासाठी",
|
||||
"firstrun_email_input_placeholder": "ईमेल",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
|
||||
"firstrun_invalid_input": "वैध ईमेल आवश्यक",
|
||||
"firstrun_extra_legal_links": "पुढे जाताना आपण {terms} आणि {privacy} यांना संमती देता.",
|
||||
"firstrun_terms_of_service": "सेवा अटी",
|
||||
"firstrun_privacy_notice": "गोपनीयता सूचना",
|
||||
"firstrun_continue_to_login": "पुढे चला",
|
||||
|
|
|
@ -97,7 +97,7 @@ window.gActivityStreamStrings = {
|
|||
"firstrun_form_header": "Vnesite e-poštni naslov",
|
||||
"firstrun_form_sub_header": "za nadaljevanje v Firefox Sync.",
|
||||
"firstrun_email_input_placeholder": "E-pošta",
|
||||
"firstrun_invalid_input": "Valid email required",
|
||||
"firstrun_invalid_input": "Zahtevan je veljaven e-poštni naslov",
|
||||
"firstrun_extra_legal_links": "Z nadaljevanjem se strinjate s {terms} in {privacy}.",
|
||||
"firstrun_terms_of_service": "Pogoji uporabe",
|
||||
"firstrun_privacy_notice": "Obvestilom o zasebnosti",
|
||||
|
|
|
@ -56,5 +56,8 @@ window.gActivityStreamPrerenderedState = {
|
|||
"order": 2,
|
||||
"initialized": false
|
||||
}
|
||||
]
|
||||
],
|
||||
"Pocket": {
|
||||
"waitingForSpoc": true
|
||||
}
|
||||
};
|
||||
|
|
|
@ -110,6 +110,16 @@ add_task(async function checkProfileAgeReset() {
|
|||
"should select correct item by profile age reset");
|
||||
});
|
||||
|
||||
add_task(async function checkCurrentDate() {
|
||||
let message = {id: "foo", targeting: `currentDate < '${new Date(Date.now() + 1000)}'|date`};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
|
||||
"should select message based on currentDate < timestamp");
|
||||
|
||||
message = {id: "foo", targeting: `currentDate > '${new Date(Date.now() - 1000)}'|date`};
|
||||
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
|
||||
"should select message based on currentDate > timestamp");
|
||||
});
|
||||
|
||||
add_task(async function checkhasFxAccount() {
|
||||
await pushPrefs(["services.sync.username", "someone@foo.com"]);
|
||||
is(await ASRouterTargeting.Environment.hasFxAccount, true,
|
||||
|
@ -291,3 +301,12 @@ add_task(async function check_sync() {
|
|||
is(await ASRouterTargeting.Environment.sync.totalDevices, Services.prefs.getIntPref("services.sync.numClients", 0),
|
||||
"should return correct mobileDevices info");
|
||||
});
|
||||
|
||||
add_task(async function check_onboarding_cohort() {
|
||||
Services.prefs.setStringPref("browser.newtabpage.activity-stream.asrouter.messageProviders", JSON.stringify([{id: "onboarding", enabled: true, cohort: 1}]));
|
||||
is(await ASRouterTargeting.Environment.isInExperimentCohort, 1);
|
||||
Services.prefs.setStringPref("browser.newtabpage.activity-stream.asrouter.messageProviders", JSON.stringify(17));
|
||||
is(await ASRouterTargeting.Environment.isInExperimentCohort, 0);
|
||||
Services.prefs.setStringPref("browser.newtabpage.activity-stream.asrouter.messageProviders", JSON.stringify([{id: "onboarding", enabled: true, cohort: "hello"}]));
|
||||
is(await ASRouterTargeting.Environment.isInExperimentCohort, 0);
|
||||
});
|
||||
|
|
|
@ -0,0 +1,36 @@
|
|||
import {combineReducers, createStore} from "redux";
|
||||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
import {enableASRouterContent} from "content-src/lib/asroutercontent.js";
|
||||
import {reducers} from "common/Reducers.jsm";
|
||||
|
||||
describe("asrouter", () => {
|
||||
let sandbox;
|
||||
let store;
|
||||
let asrouterContent;
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
store = createStore(combineReducers(reducers));
|
||||
sandbox.spy(store, "subscribe");
|
||||
});
|
||||
it("should initialize asrouter once if asrouterExperimentEnabled is true", () => {
|
||||
({asrouterContent} = enableASRouterContent(store, {
|
||||
init: sandbox.stub(),
|
||||
uninit: sandbox.stub(),
|
||||
initialized: false
|
||||
}));
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
|
||||
|
||||
assert.calledOnce(asrouterContent.init);
|
||||
});
|
||||
it("should uninitialize asrouter if asrouterExperimentEnabled pref is turned off", () => {
|
||||
({asrouterContent} = enableASRouterContent(store, {
|
||||
init: sandbox.stub(),
|
||||
uninit: sandbox.stub(),
|
||||
initialized: true
|
||||
}));
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
|
||||
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: false}});
|
||||
assert.calledOnce(asrouterContent.uninit);
|
||||
});
|
||||
});
|
|
@ -6,15 +6,17 @@ import {
|
|||
FAKE_LOCAL_PROVIDERS,
|
||||
FAKE_REMOTE_MESSAGES,
|
||||
FAKE_REMOTE_PROVIDER,
|
||||
FAKE_REMOTE_SETTINGS_PROVIDER,
|
||||
FakeRemotePageManager,
|
||||
PARENT_TO_CHILD_MESSAGE_NAME
|
||||
} from "./constants";
|
||||
import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
|
||||
import {CFRPageActions} from "lib/CFRPageActions.jsm";
|
||||
import {GlobalOverrider} from "test/unit/utils";
|
||||
import ProviderResponseSchema from "content-src/asrouter/schemas/provider-response.schema.json";
|
||||
|
||||
const MESSAGE_PROVIDER_PREF_NAME = "browser.newtabpage.activity-stream.asrouter.messageProviders";
|
||||
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER];
|
||||
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER, FAKE_REMOTE_SETTINGS_PROVIDER];
|
||||
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
|
||||
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
@ -33,8 +35,10 @@ describe("ASRouter", () => {
|
|||
let Router;
|
||||
let channel;
|
||||
let sandbox;
|
||||
let blockList;
|
||||
let impressions;
|
||||
let messageBlockList;
|
||||
let providerBlockList;
|
||||
let messageImpressions;
|
||||
let providerImpressions;
|
||||
let fetchStub;
|
||||
let clock;
|
||||
let getStringPrefStub;
|
||||
|
@ -43,8 +47,10 @@ describe("ASRouter", () => {
|
|||
|
||||
function createFakeStorage() {
|
||||
const getStub = sandbox.stub();
|
||||
getStub.withArgs("blockList").returns(Promise.resolve(blockList));
|
||||
getStub.withArgs("impressions").returns(Promise.resolve(impressions));
|
||||
getStub.withArgs("messageBlockList").returns(Promise.resolve(messageBlockList));
|
||||
getStub.withArgs("providerBlockList").returns(Promise.resolve(providerBlockList));
|
||||
getStub.withArgs("messageImpressions").returns(Promise.resolve(messageImpressions));
|
||||
getStub.withArgs("providerImpressions").returns(Promise.resolve(providerImpressions));
|
||||
return {
|
||||
get: getStub,
|
||||
set: sandbox.stub().returns(Promise.resolve())
|
||||
|
@ -66,8 +72,10 @@ describe("ASRouter", () => {
|
|||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
blockList = [];
|
||||
impressions = {};
|
||||
messageBlockList = [];
|
||||
providerBlockList = [];
|
||||
messageImpressions = {};
|
||||
providerImpressions = {};
|
||||
sandbox = sinon.sandbox.create();
|
||||
clock = sandbox.useFakeTimers();
|
||||
fetchStub = sandbox.stub(global, "fetch")
|
||||
|
@ -100,23 +108,23 @@ describe("ASRouter", () => {
|
|||
assert.calledOnce(addObserverStub);
|
||||
assert.calledWith(addObserverStub, MESSAGE_PROVIDER_PREF_NAME);
|
||||
});
|
||||
it("should set state.blockList to the block list in persistent storage", async () => {
|
||||
blockList = ["foo"];
|
||||
it("should set state.messageBlockList to the block list in persistent storage", async () => {
|
||||
messageBlockList = ["foo"];
|
||||
Router = new _ASRouter({providers: FAKE_PROVIDERS});
|
||||
await Router.init(channel, createFakeStorage(), dispatchStub);
|
||||
|
||||
assert.deepEqual(Router.state.blockList, ["foo"]);
|
||||
assert.deepEqual(Router.state.messageBlockList, ["foo"]);
|
||||
});
|
||||
it("should set state.impressions to the impressions object in persistent storage", async () => {
|
||||
// Note that impressions are only kept if a message exists in router and has a .frequency property,
|
||||
it("should set state.messageImpressions to the messageImpressions object in persistent storage", async () => {
|
||||
// Note that messageImpressions are only kept if a message exists in router and has a .frequency property,
|
||||
// otherwise they will be cleaned up by .cleanupImpressions()
|
||||
const testMessage = {id: "foo", frequency: {lifetimeCap: 10}};
|
||||
impressions = {foo: [0, 1, 2]};
|
||||
messageImpressions = {foo: [0, 1, 2]};
|
||||
|
||||
Router = new _ASRouter({providers: [{id: "onboarding", type: "local", messages: [testMessage]}]});
|
||||
await Router.init(channel, createFakeStorage(), dispatchStub);
|
||||
|
||||
assert.deepEqual(Router.state.impressions, impressions);
|
||||
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
|
||||
});
|
||||
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
|
||||
Router = new _ASRouter(MESSAGE_PROVIDER_PREF_NAME, FAKE_LOCAL_PROVIDERS);
|
||||
|
@ -137,7 +145,7 @@ describe("ASRouter", () => {
|
|||
});
|
||||
it("should update the list of providers on pref change", async () => {
|
||||
const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {url: "baz.com"});
|
||||
setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider]);
|
||||
setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider, FAKE_REMOTE_SETTINGS_PROVIDER]);
|
||||
|
||||
const {length} = Router.state.providers;
|
||||
await Router.observe("", "", MESSAGE_PROVIDER_PREF_NAME);
|
||||
|
@ -177,7 +185,7 @@ describe("ASRouter", () => {
|
|||
|
||||
it("should not trigger an update if not enough time has passed for a provider", async () => {
|
||||
await createRouterAndInit([
|
||||
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300}
|
||||
{id: "remotey", type: "remote", enabled: true, url: "http://fake.com/endpoint", updateCycleInMs: 300}
|
||||
]);
|
||||
|
||||
const previousState = Router.state;
|
||||
|
@ -189,7 +197,7 @@ describe("ASRouter", () => {
|
|||
});
|
||||
it("should not trigger an update if we only have local providers", async () => {
|
||||
await createRouterAndInit([
|
||||
{id: "foo", type: "local", messages: FAKE_LOCAL_MESSAGES}
|
||||
{id: "foo", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES}
|
||||
]);
|
||||
|
||||
const previousState = Router.state;
|
||||
|
@ -202,8 +210,8 @@ describe("ASRouter", () => {
|
|||
it("should update messages for a provider if enough time has passed, without removing messages for other providers", async () => {
|
||||
const NEW_MESSAGES = [{id: "new_123"}];
|
||||
await createRouterAndInit([
|
||||
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300},
|
||||
{id: "alocalprovider", type: "local", messages: FAKE_LOCAL_MESSAGES}
|
||||
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", enabled: true, updateCycleInMs: 300},
|
||||
{id: "alocalprovider", type: "local", enabled: true, messages: FAKE_LOCAL_MESSAGES}
|
||||
]);
|
||||
fetchStub
|
||||
.withArgs("http://fake.com/endpoint")
|
||||
|
@ -222,7 +230,7 @@ describe("ASRouter", () => {
|
|||
|
||||
/* eslint-disable object-curly-newline */ /* eslint-disable object-property-newline */
|
||||
await createRouterAndInit([
|
||||
{id: "foo", type: "local", messages: [
|
||||
{id: "foo", type: "local", enabled: true, messages: [
|
||||
{id: "foo", template: "simple_template", trigger: {id: "firstRun"}, content: {title: "Foo", body: "Foo123"}},
|
||||
{id: "bar1", template: "simple_template", trigger: {id: "openURL", params: ["www.mozilla.org", "www.mozilla.com"]}, content: {title: "Bar1", body: "Bar123"}},
|
||||
{id: "bar2", template: "simple_template", trigger: {id: "openURL", params: ["www.example.com"]}, content: {title: "Bar2", body: "Bar123"}}
|
||||
|
@ -236,6 +244,10 @@ describe("ASRouter", () => {
|
|||
assert.calledWithExactly(ASRouterTriggerListeners.get("openURL").init,
|
||||
Router._triggerHandler, ["www.example.com"]);
|
||||
});
|
||||
it("should gracefully handle RemoteSettings blowing up", async () => {
|
||||
sandbox.stub(MessageLoaderUtils, "_getRemoteSettingsMessages").rejects("fake error");
|
||||
await createRouterAndInit();
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_updateMessageProviders", () => {
|
||||
|
@ -243,7 +255,7 @@ describe("ASRouter", () => {
|
|||
// If this test fails, you need to update the constant STARTPAGE_VERSION in
|
||||
// ASRouter.jsm to match the `version` property of provider-response-schema.json
|
||||
const expectedStartpageVersion = ProviderResponseSchema.version;
|
||||
const provider = {id: "foo", type: "remote", url: "https://www.mozilla.org/%STARTPAGE_VERSION%/"};
|
||||
const provider = {id: "foo", enabled: true, type: "remote", url: "https://www.mozilla.org/%STARTPAGE_VERSION%/"};
|
||||
setMessageProviderPref([provider]);
|
||||
Router._updateMessageProviders();
|
||||
assert.equal(Router.state.providers[0].url, `https://www.mozilla.org/${expectedStartpageVersion}/`);
|
||||
|
@ -254,19 +266,29 @@ describe("ASRouter", () => {
|
|||
const stub = sandbox.stub(global.Services.urlFormatter, "formatURL")
|
||||
.withArgs(url)
|
||||
.returns(replacedUrl);
|
||||
const provider = {id: "foo", type: "remote", url};
|
||||
const provider = {id: "foo", enabled: true, type: "remote", url};
|
||||
setMessageProviderPref([provider]);
|
||||
Router._updateMessageProviders();
|
||||
assert.calledOnce(stub);
|
||||
assert.calledWithExactly(stub, url);
|
||||
assert.equal(Router.state.providers[0].url, replacedUrl);
|
||||
});
|
||||
it("should only add the providers that are enabled", () => {
|
||||
const providers = [
|
||||
{id: "foo", enabled: false, type: "remote", url: "https://www.foo.com/"},
|
||||
{id: "bar", enabled: true, type: "remote", url: "https://www.bar.com/"}
|
||||
];
|
||||
setMessageProviderPref(providers);
|
||||
Router._updateMessageProviders();
|
||||
assert.equal(Router.state.providers.length, 1);
|
||||
assert.equal(Router.state.providers[0].id, providers[1].id);
|
||||
});
|
||||
});
|
||||
|
||||
describe("blocking", () => {
|
||||
it("should not return a blocked message", async () => {
|
||||
// Block all messages except the first
|
||||
await Router.setState(() => ({blockList: ALL_MESSAGE_IDS.slice(1)}));
|
||||
await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS.slice(1)}));
|
||||
const targetStub = {sendAsyncMessage: sandbox.stub()};
|
||||
|
||||
await Router.sendNextMessage(targetStub);
|
||||
|
@ -274,8 +296,19 @@ describe("ASRouter", () => {
|
|||
assert.calledOnce(targetStub.sendAsyncMessage);
|
||||
assert.equal(Router.state.lastMessageId, ALL_MESSAGE_IDS[0]);
|
||||
});
|
||||
it("should not return a message from a blocked provider", async () => {
|
||||
// There are only two providers; block the FAKE_LOCAL_PROVIDER, leaving
|
||||
// only FAKE_REMOTE_PROVIDER unblocked, which provides only one message
|
||||
await Router.setState(() => ({providerBlockList: [FAKE_LOCAL_PROVIDER.id]}));
|
||||
const targetStub = {sendAsyncMessage: sandbox.stub()};
|
||||
|
||||
await Router.sendNextMessage(targetStub);
|
||||
|
||||
assert.calledOnce(targetStub.sendAsyncMessage);
|
||||
assert.equal(Router.state.lastMessageId, FAKE_REMOTE_MESSAGES[0].id);
|
||||
});
|
||||
it("should not return a message if all messages are blocked", async () => {
|
||||
await Router.setState(() => ({blockList: ALL_MESSAGE_IDS}));
|
||||
await Router.setState(() => ({messageBlockList: ALL_MESSAGE_IDS}));
|
||||
const targetStub = {sendAsyncMessage: sandbox.stub()};
|
||||
|
||||
await Router.sendNextMessage(targetStub);
|
||||
|
@ -400,59 +433,84 @@ describe("ASRouter", () => {
|
|||
});
|
||||
|
||||
describe("#onMessage: BLOCK_MESSAGE_BY_ID", () => {
|
||||
it("should add the id to the blockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
|
||||
it("should add the id to the messageBlockList and broadcast a CLEAR_MESSAGE message with the id", async () => {
|
||||
await Router.setState({lastMessageId: "foo"});
|
||||
const msg = fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.isTrue(Router.state.blockList.includes("foo"));
|
||||
assert.isTrue(Router.state.messageBlockList.includes("foo"));
|
||||
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_MESSAGE", data: {id: "foo"}});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onMessage: BLOCK_PROVIDER_BY_ID", () => {
|
||||
it("should add the provider id to the providerBlockList and broadcast a CLEAR_PROVIDER with the provider id", async () => {
|
||||
const msg = fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "bar"}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.isTrue(Router.state.providerBlockList.includes("bar"));
|
||||
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: "bar"}});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onMessage: BLOCK_BUNDLE", () => {
|
||||
it("should add all the ids in the bundle to the blockList and send a CLEAR_BUNDLE message", async () => {
|
||||
it("should add all the ids in the bundle to the messageBlockList and send a CLEAR_BUNDLE message", async () => {
|
||||
const bundleIds = [FAKE_BUNDLE[0].id, FAKE_BUNDLE[1].id];
|
||||
await Router.setState({lastMessageId: "foo"});
|
||||
const msg = fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
|
||||
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
||||
assert.calledWith(channel.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_BUNDLE"});
|
||||
assert.calledWithExactly(Router._storage.set, "blockList", bundleIds);
|
||||
assert.calledWithExactly(Router._storage.set, "messageBlockList", bundleIds);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onMessage: UNBLOCK_MESSAGE_BY_ID", () => {
|
||||
it("should remove the id from the blockList", async () => {
|
||||
it("should remove the id from the messageBlockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
||||
assert.isTrue(Router.state.blockList.includes("foo"));
|
||||
assert.isTrue(Router.state.messageBlockList.includes("foo"));
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
||||
|
||||
assert.isFalse(Router.state.blockList.includes("foo"));
|
||||
assert.isFalse(Router.state.messageBlockList.includes("foo"));
|
||||
});
|
||||
it("should save the blockList", async () => {
|
||||
it("should save the messageBlockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_MESSAGE_BY_ID", data: {id: "foo"}}));
|
||||
|
||||
assert.calledWithExactly(Router._storage.set, "blockList", []);
|
||||
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onMessage: UNBLOCK_PROVIDER_BY_ID", () => {
|
||||
it("should remove the id from the providerBlockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
|
||||
assert.isTrue(Router.state.providerBlockList.includes("foo"));
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
|
||||
|
||||
assert.isFalse(Router.state.providerBlockList.includes("foo"));
|
||||
});
|
||||
it("should save the providerBlockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_PROVIDER_BY_ID", data: {id: "foo"}}));
|
||||
|
||||
assert.calledWithExactly(Router._storage.set, "providerBlockList", []);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#onMessage: UNBLOCK_BUNDLE", () => {
|
||||
it("should remove all the ids in the bundle from the blockList", async () => {
|
||||
it("should remove all the ids in the bundle from the messageBlockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "BLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
||||
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isTrue(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
|
||||
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isTrue(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
||||
|
||||
assert.isFalse(Router.state.blockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isFalse(Router.state.blockList.includes(FAKE_BUNDLE[1].id));
|
||||
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[0].id));
|
||||
assert.isFalse(Router.state.messageBlockList.includes(FAKE_BUNDLE[1].id));
|
||||
});
|
||||
it("should save the blockList", async () => {
|
||||
it("should save the messageBlockList", async () => {
|
||||
await Router.onMessage(fakeAsyncMessage({type: "UNBLOCK_BUNDLE", data: {bundle: FAKE_BUNDLE}}));
|
||||
|
||||
assert.calledWithExactly(Router._storage.set, "blockList", []);
|
||||
assert.calledWithExactly(Router._storage.set, "messageBlockList", []);
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -548,15 +606,15 @@ describe("ASRouter", () => {
|
|||
await Router.onMessage(msg);
|
||||
|
||||
assert.calledOnce(Router._findMessage);
|
||||
assert.deepEqual(Router._findMessage.firstCall.args[2], {id: "firstRun"});
|
||||
assert.deepEqual(Router._findMessage.firstCall.args[1], {id: "firstRun"});
|
||||
});
|
||||
it("consider the trigger when picking a message", async () => {
|
||||
let messages = [
|
||||
const messages = [
|
||||
{id: "foo1", template: "simple_template", bundled: 1, trigger: {id: "foo"}, content: {title: "Foo1", body: "Foo123-1"}}
|
||||
];
|
||||
|
||||
const {target} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
|
||||
let message = await Router._findMessage(messages, target, {id: "foo"});
|
||||
const {data} = fakeAsyncMessage({type: "TRIGGER", data: {trigger: {id: "foo"}}});
|
||||
const message = await Router._findMessage(messages, data.data.trigger);
|
||||
assert.equal(message, messages[0]);
|
||||
});
|
||||
it("should pick a message with the right targeting and trigger", async () => {
|
||||
|
@ -583,6 +641,17 @@ describe("ASRouter", () => {
|
|||
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: testMessage});
|
||||
});
|
||||
|
||||
it("should call CFRPageActions.addRecommendation if the template is cfr_action", async () => {
|
||||
sandbox.stub(CFRPageActions, "addRecommendation");
|
||||
const testMessage = {id: "foo", template: "cfr_doorhanger"};
|
||||
await Router.setState({messages: [testMessage]});
|
||||
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: testMessage.id}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.notCalled(msg.target.sendAsyncMessage);
|
||||
assert.calledOnce(CFRPageActions.addRecommendation);
|
||||
});
|
||||
|
||||
it("should broadcast CLEAR_ALL if provided id did not resolve to a message", async () => {
|
||||
const msg = fakeAsyncMessage({type: "OVERRIDE_MESSAGE", data: {id: -1}});
|
||||
await Router.onMessage(msg);
|
||||
|
@ -600,24 +669,23 @@ describe("ASRouter", () => {
|
|||
assert.calledWith(msg.target.browser.ownerGlobal.OpenBrowserWindow, {private: true});
|
||||
});
|
||||
it("should call openLinkIn with the correct params on OPEN_URL", async () => {
|
||||
sinon.spy(Router, "openLinkIn");
|
||||
let [testMessage] = Router.state.messages;
|
||||
testMessage.button_action = {type: "OPEN_URL", data: {url: "some/url.com"}};
|
||||
const msg = fakeExecuteUserAction(testMessage.button_action);
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.calledWith(Router.openLinkIn, "some/url.com", msg.target, {isPrivate: false, where: "tabshifted"});
|
||||
assert.calledOnce(msg.target.browser.ownerGlobal.openLinkIn);
|
||||
assert.calledWith(msg.target.browser.ownerGlobal.openLinkIn,
|
||||
"some/url.com", "tabshifted", {"private": false, "triggeringPrincipal": undefined});
|
||||
});
|
||||
it("should call openLinkIn with the correct params on OPEN_ABOUT_PAGE", async () => {
|
||||
sinon.spy(Router, "openLinkIn");
|
||||
let [testMessage] = Router.state.messages;
|
||||
testMessage.button_action = {type: "OPEN_ABOUT_PAGE", data: {page: "something"}};
|
||||
const msg = fakeExecuteUserAction(testMessage.button_action);
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.calledWith(Router.openLinkIn, `about:something`, msg.target, {isPrivate: false, trusted: true, where: "tab"});
|
||||
assert.calledOnce(msg.target.browser.ownerGlobal.openTrustedLinkIn);
|
||||
assert.calledWith(msg.target.browser.ownerGlobal.openTrustedLinkIn, "about:something", "tab");
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -633,6 +701,21 @@ describe("ASRouter", () => {
|
|||
});
|
||||
});
|
||||
|
||||
describe("#dispatch(action, target)", () => {
|
||||
it("should an action and target to onMessage", async () => {
|
||||
// use the IMPRESSION action to make sure actions are actually getting processed
|
||||
sandbox.stub(Router, "addImpression");
|
||||
sandbox.spy(Router, "onMessage");
|
||||
const target = {};
|
||||
const action = {type: "IMPRESSION"};
|
||||
|
||||
Router.dispatch(action, target);
|
||||
|
||||
assert.calledWith(Router.onMessage, {data: action, target});
|
||||
assert.calledOnce(Router.addImpression);
|
||||
});
|
||||
});
|
||||
|
||||
describe("_triggerHandler", () => {
|
||||
it("should call #onMessage with the correct trigger", () => {
|
||||
sinon.spy(Router, "onMessage");
|
||||
|
@ -640,7 +723,7 @@ describe("ASRouter", () => {
|
|||
const trigger = {id: "FAKE_TRIGGER", param: "some fake param"};
|
||||
Router._triggerHandler(target, trigger);
|
||||
assert.calledOnce(Router.onMessage);
|
||||
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", trigger}});
|
||||
assert.calledWithExactly(Router.onMessage, {target, data: {type: "TRIGGER", data: {trigger}}});
|
||||
});
|
||||
});
|
||||
|
||||
|
@ -671,26 +754,181 @@ describe("ASRouter", () => {
|
|||
});
|
||||
|
||||
describe("impressions", () => {
|
||||
it("should add an impression and update _storage with the current time if the message frequency caps", async () => {
|
||||
clock.tick(42);
|
||||
const msg = fakeAsyncMessage({type: "IMPRESSION", data: {id: "foo", frequency: {lifetime: 5}}});
|
||||
await Router.onMessage(msg);
|
||||
|
||||
assert.isArray(Router.state.impressions.foo);
|
||||
assert.deepEqual(Router.state.impressions.foo, [42]);
|
||||
assert.calledWith(Router._storage.set, "impressions", {foo: [42]});
|
||||
async function addProviderWithFrequency(id, frequency) {
|
||||
await Router.setState(state => {
|
||||
const newProvider = {id, frequency};
|
||||
const providers = [...state.providers, newProvider];
|
||||
return {providers};
|
||||
});
|
||||
it("should not add an impression if the message doesn't have frequency caps", async () => {
|
||||
}
|
||||
|
||||
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.impressions, "foo");
|
||||
assert.notProperty(Router.state.messageImpressions, "foo");
|
||||
assert.notCalled(Router._storage.set);
|
||||
});
|
||||
describe("getLongestPeriod", () => {
|
||||
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);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#isBelowFrequencyCaps", () => {
|
||||
it("should call #_isBelowItemFrequencyCap for the message and for the provider with the correct impressions and arguments", async () => {
|
||||
sinon.spy(Router, "_isBelowItemFrequencyCap");
|
||||
|
||||
const MAX_MESSAGE_LIFETIME_CAP = 100; // Defined in ASRouter
|
||||
const fooMessageImpressions = [0, 1];
|
||||
const barProviderImpressions = [0, 1, 2];
|
||||
|
||||
const message = {id: "foo", provider: "bar", frequency: {lifetime: 3}};
|
||||
const provider = {id: "bar", frequency: {lifetime: 5}};
|
||||
|
||||
await Router.setState(state => {
|
||||
// Add provider
|
||||
const providers = [...state.providers, provider];
|
||||
// Add fooMessageImpressions
|
||||
const messageImpressions = Object.assign({}, state.messageImpressions); // eslint-disable-line no-shadow
|
||||
messageImpressions.foo = fooMessageImpressions;
|
||||
// Add barProviderImpressions
|
||||
const providerImpressions = Object.assign({}, state.providerImpressions); // eslint-disable-line no-shadow
|
||||
providerImpressions.bar = barProviderImpressions;
|
||||
return {providers, messageImpressions, providerImpressions};
|
||||
});
|
||||
|
||||
await Router.isBelowFrequencyCaps(message);
|
||||
|
||||
assert.calledTwice(Router._isBelowItemFrequencyCap);
|
||||
assert.calledWithExactly(Router._isBelowItemFrequencyCap, message, fooMessageImpressions, MAX_MESSAGE_LIFETIME_CAP);
|
||||
assert.calledWithExactly(Router._isBelowItemFrequencyCap, provider, barProviderImpressions);
|
||||
});
|
||||
});
|
||||
|
||||
describe("#_isBelowItemFrequencyCap", () => {
|
||||
it("should return false if the # of impressions exceeds the maxLifetimeCap", () => {
|
||||
const item = {id: "foo", frequency: {lifetime: 5}};
|
||||
const impressions = [0, 1];
|
||||
const maxLifetimeCap = 1;
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions, maxLifetimeCap);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
|
||||
describe("lifetime frequency caps", () => {
|
||||
it("should return true if .frequency is not defined on the item", () => {
|
||||
const item = {id: "foo"};
|
||||
const impressions = [0, 1];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return true if there are no impressions", () => {
|
||||
const item = {id: "foo", frequency: {lifetime: 10, custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return true if the # of impressions is less than .frequency.lifetime of the item", () => {
|
||||
const item = {id: "foo", frequency: {lifetime: 3}};
|
||||
const impressions = [0, 1];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if the # of impressions is equal to .frequency.lifetime of the item", async () => {
|
||||
const item = {id: "foo", frequency: {lifetime: 3}};
|
||||
const impressions = [0, 1, 2];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return false if the # of impressions is greater than .frequency.lifetime of the item", async () => {
|
||||
const item = {id: "foo", frequency: {lifetime: 3}};
|
||||
const impressions = [0, 1, 2, 3];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
});
|
||||
|
||||
describe("custom frequency caps", () => {
|
||||
it("should return true if impressions in the time period < the cap and total impressions < the lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const item = {id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
|
||||
const impressions = [0, ONE_DAY + 1];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if impressions in the time period > the cap and total impressions < the lifetime cap", () => {
|
||||
clock.tick(200);
|
||||
const item = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}], lifetime: 3}};
|
||||
const impressions = [0, 160, 161];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return false if impressions in one of the time periods > the cap and total impressions < the lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 200);
|
||||
const itemTrue = {id: "msg2", frequency: {custom: [{period: 100, cap: 2}]}};
|
||||
const itemFalse = {id: "msg1", frequency: {custom: [{period: 100, cap: 2}, {period: ONE_DAY, cap: 3}]}};
|
||||
const impressions = [0, ONE_DAY + 160, ONE_DAY - 100, ONE_DAY - 200];
|
||||
assert.isTrue(Router._isBelowItemFrequencyCap(itemTrue, impressions));
|
||||
assert.isFalse(Router._isBelowItemFrequencyCap(itemFalse, impressions));
|
||||
});
|
||||
it("should return false if impressions in the time period < the cap and total impressions > the lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}], lifetime: 3}};
|
||||
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return true if daily impressions < the daily cap and there is no lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [0, 1, 2, 3, ONE_DAY + 1];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if daily impressions > the daily cap and there is no lifetime cap", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const item = {id: "msg1", frequency: {custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3];
|
||||
const result = Router._isBelowItemFrequencyCap(item, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should allow the 'daily' alias for period", () => {
|
||||
clock.tick(ONE_DAY + 10);
|
||||
const item = {id: "msg1", frequency: {custom: [{period: "daily", cap: 2}]}};
|
||||
assert.isFalse(Router._isBelowItemFrequencyCap(item, [0, 1, 2, 3, ONE_DAY + 1, ONE_DAY + 2, ONE_DAY + 3]));
|
||||
assert.isTrue(Router._isBelowItemFrequencyCap(item, [0, 1, 2, 3, ONE_DAY + 1]));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe("#getLongestPeriod", () => {
|
||||
it("should return the period if there is only one definition", () => {
|
||||
const message = {id: "foo", frequency: {custom: [{period: 200, cap: 2}]}};
|
||||
assert.equal(Router.getLongestPeriod(message), 200);
|
||||
|
@ -708,58 +946,59 @@ describe("ASRouter", () => {
|
|||
assert.isNull(Router.getLongestPeriod(message));
|
||||
});
|
||||
});
|
||||
|
||||
describe("cleanup on init", () => {
|
||||
it("should clear impressions for messages which do not exist in state.messages", async () => {
|
||||
it("should clear messageImpressions for messages which do not exist in state.messages", async () => {
|
||||
const messages = [{id: "foo", frequency: {lifetime: 10}}];
|
||||
impressions = {foo: [0], bar: [0, 1]};
|
||||
messageImpressions = {foo: [0], bar: [0, 1]};
|
||||
// Impressions for "bar" should be removed since that id does not exist in messages
|
||||
const result = {foo: [0]};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
|
||||
assert.calledWith(Router._storage.set, "messageImpressions", result);
|
||||
assert.deepEqual(Router.state.messageImpressions, result);
|
||||
});
|
||||
it("should clear impressions older than the period if no lifetime impression cap is included", async () => {
|
||||
it("should clear messageImpressions older than the period if no lifetime impression cap is included", async () => {
|
||||
const CURRENT_TIME = ONE_DAY * 2;
|
||||
clock.tick(CURRENT_TIME);
|
||||
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}]}}];
|
||||
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
|
||||
messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
|
||||
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
|
||||
const result = {foo: [CURRENT_TIME - 10]};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
|
||||
assert.calledWith(Router._storage.set, "messageImpressions", result);
|
||||
assert.deepEqual(Router.state.messageImpressions, result);
|
||||
});
|
||||
it("should clear impressions older than the longest period if no lifetime impression cap is included", async () => {
|
||||
it("should clear messageImpressions older than the longest period if no lifetime impression cap is included", async () => {
|
||||
const CURRENT_TIME = ONE_DAY * 2;
|
||||
clock.tick(CURRENT_TIME);
|
||||
const messages = [{id: "foo", frequency: {custom: [{period: ONE_DAY, cap: 5}, {period: 100, cap: 2}]}}];
|
||||
impressions = {foo: [0, 1, CURRENT_TIME - 10]};
|
||||
messageImpressions = {foo: [0, 1, CURRENT_TIME - 10]};
|
||||
// Only 0 and 1 are more than 24 hours before CURRENT_TIME
|
||||
const result = {foo: [CURRENT_TIME - 10]};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
|
||||
assert.calledWith(Router._storage.set, "messageImpressions", result);
|
||||
assert.deepEqual(Router.state.messageImpressions, result);
|
||||
});
|
||||
it("should clear impressions if they are not properly formatted", async () => {
|
||||
it("should clear messageImpressions if they are not properly formatted", async () => {
|
||||
const messages = [{id: "foo", frequency: {lifetime: 10}}];
|
||||
// this is impromperly formatted since impressions are supposed to be an array
|
||||
impressions = {foo: 0};
|
||||
// this is impromperly formatted since messageImpressions are supposed to be an array
|
||||
messageImpressions = {foo: 0};
|
||||
const result = {};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
assert.calledWith(Router._storage.set, "impressions", result);
|
||||
assert.deepEqual(Router.state.impressions, result);
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
|
||||
assert.calledWith(Router._storage.set, "messageImpressions", result);
|
||||
assert.deepEqual(Router.state.messageImpressions, result);
|
||||
});
|
||||
it("should not clear impressions for messages which do exist in state.messages", async () => {
|
||||
it("should not clear messageImpressions for messages which do exist in state.messages", async () => {
|
||||
const messages = [{id: "foo", frequency: {lifetime: 10}}, {id: "bar", frequency: {lifetime: 10}}];
|
||||
impressions = {foo: [0], bar: []};
|
||||
messageImpressions = {foo: [0], bar: []};
|
||||
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages}]);
|
||||
await createRouterAndInit([{id: "onboarding", type: "local", messages, enabled: true}]);
|
||||
assert.notCalled(Router._storage.set);
|
||||
assert.deepEqual(Router.state.impressions, impressions);
|
||||
assert.deepEqual(Router.state.messageImpressions, messageImpressions);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -1,100 +1,27 @@
|
|||
import {ASRouterTargeting} from "lib/ASRouterTargeting.jsm";
|
||||
const ONE_DAY = 24 * 60 * 60 * 1000;
|
||||
|
||||
// Note that tests for the ASRouterTargeting environment can be found in
|
||||
// test/functional/mochitest/browser_asrouter_targeting.js
|
||||
|
||||
describe("ASRouterTargeting#isBelowFrequencyCap", () => {
|
||||
describe("lifetime frequency caps", () => {
|
||||
it("should return true if .frequency is not defined on the message", () => {
|
||||
const message = {id: "msg1"};
|
||||
const impressions = [0, 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return true if there are no impressions", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 10, custom: [{period: ONE_DAY, cap: 2}]}};
|
||||
const impressions = [];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return true if the # of impressions is less than .frequency.lifetime", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 3}};
|
||||
const impressions = [0, 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isTrue(result);
|
||||
});
|
||||
it("should return false if the # of impressions is equal to .frequency.lifetime", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 2}};
|
||||
const impressions = [0, 1];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
it("should return false if the # of impressions is greater than .frequency.lifetime", () => {
|
||||
const message = {id: "msg1", frequency: {lifetime: 2}};
|
||||
const impressions = [0, 1, 2];
|
||||
const result = ASRouterTargeting.isBelowFrequencyCap(message, impressions);
|
||||
assert.isFalse(result);
|
||||
});
|
||||
});
|
||||
describe("custom frequency caps", () => {
|
||||
describe("ASRouterTargeting#isInExperimentCohort", () => {
|
||||
let sandbox;
|
||||
let clock;
|
||||
beforeEach(() => {
|
||||
sandbox = sinon.sandbox.create();
|
||||
clock = sandbox.useFakeTimers();
|
||||
});
|
||||
afterEach(() => {
|
||||
sandbox.restore();
|
||||
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 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]));
|
||||
it("should return 0 if it cannot find the pref", () => {
|
||||
sandbox.stub(global.Services.prefs, "getStringPref").returns("");
|
||||
const result = ASRouterTargeting.Environment.isInExperimentCohort;
|
||||
assert.equal(result, 0);
|
||||
});
|
||||
it("should return 0 if it fails to parse the pref", () => {
|
||||
sandbox.stub(global.Services.prefs, "getStringPref").returns(17);
|
||||
const result = ASRouterTargeting.Environment.isInExperimentCohort;
|
||||
assert.equal(result, 0);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -13,11 +13,7 @@ describe("ASRouterTriggerListeners", () => {
|
|||
function resetEnumeratorStub(windows) {
|
||||
windowEnumeratorStub
|
||||
.withArgs("navigator:browser")
|
||||
.returns({
|
||||
_count: -1,
|
||||
hasMoreElements() { this._count++; return this._count < windows.length; },
|
||||
getNext() { return windows[this._count]; }
|
||||
});
|
||||
.returns(windows);
|
||||
}
|
||||
|
||||
beforeEach(async () => {
|
||||
|
@ -121,12 +117,12 @@ describe("ASRouterTriggerListeners", () => {
|
|||
const newTriggerHandler = sinon.stub();
|
||||
openURLListener.init(newTriggerHandler, hosts);
|
||||
|
||||
const browser = {messageManager: {}};
|
||||
const browser = {};
|
||||
const webProgress = {isTopLevel: true};
|
||||
const location = "https://www.mozilla.org/something";
|
||||
openURLListener.onLocationChange(browser, webProgress, undefined, {spec: location});
|
||||
assert.calledOnce(newTriggerHandler);
|
||||
assert.calledWithExactly(newTriggerHandler, browser.messageManager, {id: "openURL", param: "www.mozilla.org"});
|
||||
assert.calledWithExactly(newTriggerHandler, browser, {id: "openURL", param: "www.mozilla.org"});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -9,13 +9,15 @@ export const FAKE_LOCAL_MESSAGES = [
|
|||
{id: "bar", template: "fancy_template", content: {title: "Foo", body: "Foo123"}},
|
||||
{id: "baz", content: {title: "Foo", body: "Foo123"}}
|
||||
];
|
||||
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", localProvider: "FAKE_LOCAL_PROVIDER"};
|
||||
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", localProvider: "FAKE_LOCAL_PROVIDER", enabled: true, cohort: 0};
|
||||
export const FAKE_LOCAL_PROVIDERS = {FAKE_LOCAL_PROVIDER: {getMessages: () => FAKE_LOCAL_MESSAGES}};
|
||||
|
||||
export const FAKE_REMOTE_MESSAGES = [
|
||||
{id: "qux", template: "simple_template", content: {title: "Qux", body: "hello world"}}
|
||||
];
|
||||
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint"};
|
||||
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint", enabled: true};
|
||||
|
||||
export const FAKE_REMOTE_SETTINGS_PROVIDER = {id: "remotey-settingsy", type: "remote-settings", bucket: "bucketname", enabled: true};
|
||||
|
||||
// Stubs methods on RemotePageManager
|
||||
export class FakeRemotePageManager {
|
||||
|
|
|
@ -0,0 +1,41 @@
|
|||
import {CFRMessageProvider} from "lib/CFRMessageProvider.jsm";
|
||||
import schema from "content-src/asrouter/templates/CFR/templates/ExtensionDoorhanger.schema.json";
|
||||
|
||||
const DEFAULT_CONTENT = {
|
||||
"notification_text": "Recommendation",
|
||||
"heading_text": "Recommended Extension",
|
||||
"info_icon": {
|
||||
"label": "why_seeing_this",
|
||||
"sumo_path": "extensionrecommendations"
|
||||
},
|
||||
"addon": {
|
||||
"title": "Addon name",
|
||||
"icon": "https://mozilla.org/icon",
|
||||
"author": "Author name",
|
||||
"amo_url": "https://example.com"
|
||||
},
|
||||
"text": "Description of addon",
|
||||
"buttons": {
|
||||
"primary": {
|
||||
"label": "btn_ok",
|
||||
"action": {
|
||||
"type": "INSTALL_ADDON_FROM_URL",
|
||||
"data": {"url": "https://example.com"}
|
||||
}
|
||||
},
|
||||
"secondary": {
|
||||
"label": "btn_cancel",
|
||||
"action": {"type": "CANCEL"}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
describe("ExtensionDoorhanger", () => {
|
||||
it("should validate DEFAULT_CONTENT", () => {
|
||||
assert.jsonSchema(DEFAULT_CONTENT, schema);
|
||||
});
|
||||
it("should validate all messages from CFRMessageProvider", () => {
|
||||
const messages = CFRMessageProvider.getMessages();
|
||||
messages.forEach(msg => assert.jsonSchema(msg.content, schema));
|
||||
});
|
||||
});
|
|
@ -1,5 +1,5 @@
|
|||
import {INITIAL_STATE, insertPinned, reducers} from "common/Reducers.jsm";
|
||||
const {TopSites, App, Snippets, Prefs, Dialog, Sections} = reducers;
|
||||
const {TopSites, App, Snippets, Prefs, Dialog, Sections, Pocket} = reducers;
|
||||
import {actionTypes as at} from "common/Actions.jsm";
|
||||
|
||||
describe("Reducers", () => {
|
||||
|
@ -601,4 +601,13 @@ describe("Reducers", () => {
|
|||
assert.deepEqual(state.blockList, []);
|
||||
});
|
||||
});
|
||||
describe("Pocket", () => {
|
||||
it("should return INITIAL_STATE by default", () => {
|
||||
assert.equal(Pocket(undefined, {type: "some_action"}), INITIAL_STATE.Pocket);
|
||||
});
|
||||
it("should set waitingForSpoc on a POCKET_WAITING_FOR_SPOC action", () => {
|
||||
const state = Pocket(undefined, {type: at.POCKET_WAITING_FOR_SPOC, data: false});
|
||||
assert.isFalse(state.waitingForSpoc);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
|
|
@ -15,6 +15,11 @@ function mountSectionWithProps(props) {
|
|||
return mountWithIntl(<Provider store={store}><Section {...props} /></Provider>);
|
||||
}
|
||||
|
||||
function mountSectionIntlWithProps(props) {
|
||||
const store = createStore(combineReducers(reducers), INITIAL_STATE);
|
||||
return mountWithIntl(<Provider store={store}><SectionIntl {...props} /></Provider>);
|
||||
}
|
||||
|
||||
describe("<Sections>", () => {
|
||||
let wrapper;
|
||||
let FAKE_SECTIONS;
|
||||
|
@ -176,21 +181,33 @@ describe("<Section>", () => {
|
|||
};
|
||||
});
|
||||
it("should not render for empty topics", () => {
|
||||
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
|
||||
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
|
||||
|
||||
assert.lengthOf(wrapper.find(".topic"), 0);
|
||||
});
|
||||
it("should render for non-empty topics", () => {
|
||||
TOP_STORIES_SECTION.topics = [{name: "topic1", url: "topic-url1"}];
|
||||
|
||||
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
|
||||
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
|
||||
|
||||
assert.lengthOf(wrapper.find(".topic"), 1);
|
||||
});
|
||||
it("should delay render of third rec to give time for potential spoc", async () => {
|
||||
TOP_STORIES_SECTION.rows = [
|
||||
{guid: 1, link: "http://localhost"},
|
||||
{guid: 2, link: "http://localhost"},
|
||||
{guid: 3, link: "http://localhost"}
|
||||
];
|
||||
wrapper = shallow(<Section Pocket={{waitingForSpoc: true}} {...TOP_STORIES_SECTION} />);
|
||||
assert.lengthOf(wrapper.find(PlaceholderCard), 1);
|
||||
|
||||
wrapper.setProps({Pocket: {waitingForSpoc: false}});
|
||||
assert.lengthOf(wrapper.find(PlaceholderCard), 0);
|
||||
});
|
||||
it("should render for uninitialized topics", () => {
|
||||
delete TOP_STORIES_SECTION.topics;
|
||||
|
||||
wrapper = mountSectionWithProps(TOP_STORIES_SECTION);
|
||||
wrapper = mountSectionIntlWithProps(TOP_STORIES_SECTION);
|
||||
|
||||
assert.lengthOf(wrapper.find(".topic"), 1);
|
||||
});
|
||||
|
|
|
@ -445,7 +445,6 @@ describe("addSnippetsSubscriber", () => {
|
|||
let store;
|
||||
let sandbox;
|
||||
let snippets;
|
||||
let asrouterContent;
|
||||
function setSnippetEnabledPref(value) {
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "feeds.snippets", value}});
|
||||
}
|
||||
|
@ -454,13 +453,7 @@ describe("addSnippetsSubscriber", () => {
|
|||
store = createStore(combineReducers(reducers));
|
||||
sandbox.spy(store, "subscribe");
|
||||
setSnippetEnabledPref(true);
|
||||
({snippets, asrouterContent} = addSnippetsSubscriber(store));
|
||||
|
||||
sandbox.spy(asrouterContent, "init");
|
||||
sandbox.spy(asrouterContent, "uninit");
|
||||
// These need to be stubbed because they do dom stuff
|
||||
sandbox.stub(asrouterContent, "_mount");
|
||||
sandbox.stub(asrouterContent, "_unmount");
|
||||
({snippets} = addSnippetsSubscriber(store));
|
||||
|
||||
sandbox.stub(snippets, "init").resolves();
|
||||
sandbox.stub(snippets, "uninit");
|
||||
|
@ -473,7 +466,6 @@ describe("addSnippetsSubscriber", () => {
|
|||
delete global.gSnippetsMap;
|
||||
});
|
||||
it("should initialize feeds.snippets pref is true and SnippetsProvider if .initialize is true", () => {
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterOnboardingCohort", value: 0}});
|
||||
store.dispatch({type: at.SNIPPETS_DATA, data: {}});
|
||||
assert.calledOnce(snippets.init);
|
||||
});
|
||||
|
@ -507,37 +499,20 @@ describe("addSnippetsSubscriber", () => {
|
|||
store.dispatch({type: at.PREF_CHANGED, data: {name: "disableSnippets", value: true}});
|
||||
assert.calledOnce(snippets.uninit);
|
||||
});
|
||||
it("should not initialize snippets if asrouterExperimentEnabled pref is true", () => {
|
||||
store.dispatch({type: "FOO"});
|
||||
it("should not initialize snippets if asrouterExperimentEnabled pref and snippets message provider pref are true", () => {
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouter.messageProviders", value: JSON.stringify([{id: "snippets", enabled: true}])}});
|
||||
store.dispatch({type: at.SNIPPETS_DATA, data: {}});
|
||||
|
||||
assert.calledOnce(store.subscribe);
|
||||
assert.notCalled(snippets.init);
|
||||
});
|
||||
describe("asrouter", () => {
|
||||
it("should initialize asrouter once if asrouterExperimentEnabled and snippets pref are both true", () => {
|
||||
store.dispatch({type: "FOO"});
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "asrouterExperimentEnabled", value: true}});
|
||||
|
||||
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);
|
||||
|
||||
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}});
|
||||
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: "asrouter.messageProviders", value: JSON.stringify([{id: "snippets", enabled: false}])}});
|
||||
store.dispatch({type: at.SNIPPETS_DATA, data: {}});
|
||||
|
||||
store.dispatch({type: at.PREF_CHANGED, data: {name: "feeds.snippets", value: false}});
|
||||
assert.calledOnce(asrouterContent.uninit);
|
||||
assert.isFalse(asrouterContent.initialized);
|
||||
});
|
||||
assert.calledOnce(store.subscribe);
|
||||
assert.calledOnce(snippets.init);
|
||||
});
|
||||
});
|
||||
|
|
|
@ -17,10 +17,7 @@ describe("Screenshots", () => {
|
|||
fakeServices = {
|
||||
wm: {
|
||||
getEnumerator() {
|
||||
return {
|
||||
hasMoreElements: () => true,
|
||||
getNext: () => {}
|
||||
};
|
||||
return Array(10);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
@ -124,7 +121,7 @@ describe("Screenshots", () => {
|
|||
describe("#_shouldGetScreenshots", () => {
|
||||
beforeEach(() => {
|
||||
let more = 2;
|
||||
sandbox.stub(global.Services.wm, "getEnumerator").returns({getNext: () => {}, hasMoreElements() { return more--; }});
|
||||
sandbox.stub(global.Services.wm, "getEnumerator").callsFake(() => Array(Math.max(more--, 0)));
|
||||
});
|
||||
it("should use private browsing utils to determine if a window is private", () => {
|
||||
Screenshots._shouldGetScreenshots();
|
||||
|
|
|
@ -486,6 +486,21 @@ describe("TelemetryFeed", () => {
|
|||
assert.propertyVal(ping, "source", "SNIPPETS");
|
||||
assert.propertyVal(ping, "event", "CLICK");
|
||||
});
|
||||
it("should drop the default client_id if includeClientID presents", async () => {
|
||||
const data = {
|
||||
action: "snippet_user_event",
|
||||
source: "SNIPPETS",
|
||||
event: "CLICK",
|
||||
message_id: "snippets_message_01",
|
||||
includeClientID: true
|
||||
};
|
||||
const action = ac.ASRouterUserEvent(data);
|
||||
const ping = await instance.createASRouterEvent(action);
|
||||
|
||||
assert.isUndefined(ping.client_id);
|
||||
assert.isUndefined(ping.includeClientID);
|
||||
assert.propertyVal(ping, "impression_id", "n/a");
|
||||
});
|
||||
});
|
||||
describe("#sendEvent", () => {
|
||||
it("should call PingCentre", async () => {
|
||||
|
|
|
@ -504,7 +504,11 @@ describe("Top Stories Feed", () => {
|
|||
|
||||
instance.store.getState = () => ({Sections: [{id: "topstories", rows: response.recommendations}], Prefs: {values: {showSponsored: true}}});
|
||||
|
||||
globals.set("Math", {random: () => 0.4});
|
||||
globals.set("Math", {
|
||||
random: () => 0.4,
|
||||
min: Math.min
|
||||
});
|
||||
instance.dispatchSpocDone = () => {};
|
||||
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
let [action] = instance.store.dispatch.firstCall.args;
|
||||
|
@ -518,11 +522,17 @@ describe("Top Stories Feed", () => {
|
|||
assert.equal(action.data.rows[2].pinned, true);
|
||||
|
||||
// Second new tab shouldn't trigger a section update event (spocsPerNewTab === 0.5)
|
||||
globals.set("Math", {random: () => 0.6});
|
||||
globals.set("Math", {
|
||||
random: () => 0.6,
|
||||
min: Math.min
|
||||
});
|
||||
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
|
||||
globals.set("Math", {random: () => 0.3});
|
||||
globals.set("Math", {
|
||||
random: () => 0.3,
|
||||
min: Math.min
|
||||
});
|
||||
instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
|
||||
assert.calledTwice(instance.store.dispatch);
|
||||
[action] = instance.store.dispatch.secondCall.args;
|
||||
|
@ -536,6 +546,7 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
it("should delay inserting spoc if stories haven't been fetched", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
instance.dispatchSpocDone = () => {};
|
||||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
|
@ -545,7 +556,10 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
|
||||
globals.set("Math", {random: () => 0.4});
|
||||
globals.set("Math", {
|
||||
random: () => 0.4,
|
||||
min: Math.min
|
||||
});
|
||||
|
||||
const response = {
|
||||
"settings": {"spocsPerNewTabs": 0.5},
|
||||
|
@ -569,6 +583,7 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
it("should not insert spoc if preffed off", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
instance.dispatchSpocDone = () => {};
|
||||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: false,
|
||||
|
@ -594,8 +609,23 @@ describe("Top Stories Feed", () => {
|
|||
assert.calledOnce(instance.shouldShowSpocs);
|
||||
assert.notCalled(instance.store.dispatch);
|
||||
});
|
||||
it("should call dispatchSpocDone when calling maybeAddSpoc", async () => {
|
||||
instance.dispatchSpocDone = sinon.spy();
|
||||
instance.storiesLoaded = true;
|
||||
await instance.onAction({type: at.NEW_TAB_REHYDRATED, meta: {fromTarget: {}}});
|
||||
assert.calledOnce(instance.dispatchSpocDone);
|
||||
assert.calledWith(instance.dispatchSpocDone, {});
|
||||
});
|
||||
it("should fire POCKET_WAITING_FOR_SPOC action with false", () => {
|
||||
instance.dispatchSpocDone({});
|
||||
assert.calledOnce(instance.store.dispatch);
|
||||
const [action] = instance.store.dispatch.firstCall.args;
|
||||
assert.equal(action.type, "POCKET_WAITING_FOR_SPOC");
|
||||
assert.equal(action.data, false);
|
||||
});
|
||||
it("should not insert spoc if user opted out", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
instance.dispatchSpocDone = () => {};
|
||||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
|
@ -620,6 +650,7 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
it("should not fail if there is no spoc", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
instance.dispatchSpocDone = () => {};
|
||||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
|
@ -629,7 +660,10 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
|
||||
globals.set("Math", {random: () => 0.4});
|
||||
globals.set("Math", {
|
||||
random: () => 0.4,
|
||||
min: Math.min
|
||||
});
|
||||
|
||||
const response = {
|
||||
"settings": {"spocsPerNewTabs": 0.5},
|
||||
|
@ -646,7 +680,10 @@ describe("Top Stories Feed", () => {
|
|||
let fetchStub = globals.sandbox.stub();
|
||||
globals.set("fetch", fetchStub);
|
||||
globals.set("NewTabUtils", {blockedLinks: {isBlocked: globals.sandbox.spy()}});
|
||||
globals.set("Math", {random: () => 0.4});
|
||||
globals.set("Math", {
|
||||
random: () => 0.4,
|
||||
min: Math.min
|
||||
});
|
||||
|
||||
const response = {
|
||||
"settings": {"spocsPerNewTabs": 0.5},
|
||||
|
@ -741,6 +778,7 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
it("should maintain frequency caps when inserting spocs", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
instance.dispatchSpocDone = () => {};
|
||||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
|
@ -805,6 +843,7 @@ describe("Top Stories Feed", () => {
|
|||
});
|
||||
it("should maintain client-side MAX_LIFETIME_CAP", async () => {
|
||||
let fetchStub = globals.sandbox.stub();
|
||||
instance.dispatchSpocDone = () => {};
|
||||
sectionsManagerStub.sections.set("topstories", {
|
||||
options: {
|
||||
show_spocs: true,
|
||||
|
|
|
@ -180,7 +180,7 @@ const TEST_GLOBAL = {
|
|||
createNullPrincipal() {},
|
||||
getSystemPrincipal() {}
|
||||
},
|
||||
wm: {getMostRecentWindow: () => window, getEnumerator: () => ({hasMoreElements: () => false})},
|
||||
wm: {getMostRecentWindow: () => window, getEnumerator: () => []},
|
||||
ww: {registerNotification() {}, unregisterNotification() {}},
|
||||
appinfo: {appBuildID: "20180710100040"}
|
||||
},
|
||||
|
@ -194,6 +194,7 @@ const TEST_GLOBAL = {
|
|||
},
|
||||
defineLazyGlobalGetters() {},
|
||||
defineLazyModuleGetter() {},
|
||||
defineLazyModuleGetters() {},
|
||||
defineLazyServiceGetter() {},
|
||||
generateQI() { return {}; }
|
||||
},
|
||||
|
|
Загрузка…
Ссылка в новой задаче