Bug 1494427 - [Export] Add locale targeting, ASR onboarding/user pref fixes, test fixes to Activity Stream r=ursula

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

--HG--
rename : browser/components/newtab/locales/ur/strings.properties => browser/components/newtab/locales/is/strings.properties
extra : moz-landing-system : lando
This commit is contained in:
k88hudson 2018-09-28 20:59:01 +00:00
parent d8e0094e97
commit ddf8f0cb90
42 changed files with 754 additions and 202 deletions

View File

@ -20,7 +20,7 @@ const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
// Automated tests ensure packaged locales are in this list. Copied output of:
// https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn-BD bn-IN br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id it ja ja-JP-macos ka kab kk km kn ko lij lo lt ltg lv mai mk ml mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr uk ur uz vi zh-CN zh-TW".split(" ");
const ACTIVITY_STREAM_BCP47 = "en-US ach an ar ast az be bg bn-BD bn-IN br bs ca cak crh cs cy da de dsb el en-CA en-GB eo es-AR es-CL es-ES es-MX et eu fa ff fi fr fy-NL ga-IE gd gl gn gu-IN he hi-IN hr hsb hu hy-AM ia id is it ja ja-JP-macos ka kab kk km kn ko lij lo lt ltg lv mai mk ml mr ms my nb-NO ne-NP nl nn-NO oc pa-IN pl pt-BR pt-PT rm ro ru si sk sl sq sr sv-SE ta te th tl tr uk ur uz vi zh-CN zh-TW".split(" ");
const ABOUT_URL = "about:newtab";
const BASE_URL = "resource://activity-stream/";

View File

@ -11,6 +11,7 @@ import {SimpleSnippet} from "./templates/SimpleSnippet/SimpleSnippet";
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
const ASR_CONTAINER_ID = "asr-newtab-container";
export const ASRouterUtils = {
addListener(listener) {
@ -40,9 +41,6 @@ export const ASRouterUtils = {
unblockBundle(bundle) {
ASRouterUtils.sendMessage({type: "UNBLOCK_BUNDLE", data: {bundle}});
},
getNextMessage() {
ASRouterUtils.sendMessage({type: "GET_NEXT_MESSAGE"});
},
overrideMessage(id) {
ASRouterUtils.sendMessage({type: "OVERRIDE_MESSAGE", data: {id}});
},
@ -50,7 +48,7 @@ export const ASRouterUtils = {
const payload = ac.ASRouterUserEvent(ping);
global.RPMSendAsyncMessage(AS_GENERAL_OUTGOING_MESSAGE_NAME, payload);
},
getEndpoint() {
getPreviewEndpoint() {
if (window.location.href.includes("endpoint")) {
const params = new URLSearchParams(window.location.href.slice(window.location.href.indexOf("endpoint")));
try {
@ -203,14 +201,14 @@ export class ASRouterUISurface extends React.PureComponent {
}
componentWillMount() {
const endpoint = ASRouterUtils.getEndpoint();
const endpoint = ASRouterUtils.getPreviewEndpoint();
ASRouterUtils.addListener(this.onMessageFromParent);
// If we are loading about:welcome we want to trigger the onboarding messages
if (this.props.document.location.href === "about:welcome") {
ASRouterUtils.sendMessage({type: "TRIGGER", data: {trigger: {id: "firstRun"}}});
} else {
ASRouterUtils.sendMessage({type: "CONNECT_UI_REQUEST", data: {endpoint}});
ASRouterUtils.sendMessage({type: "SNIPPETS_REQUEST", data: {endpoint}});
}
}
@ -234,7 +232,6 @@ export class ASRouterUISurface extends React.PureComponent {
links={this.state.message.content.links}
sendClick={this.sendClick} />}
UISurface="NEWTAB_FOOTER_BAR"
getNextMessage={ASRouterUtils.getNextMessage}
onBlock={this.onBlockById(this.state.message.id)}
onAction={ASRouterUtils.executeAction}
sendUserActionTelemetry={this.sendUserActionTelemetry} />
@ -249,7 +246,6 @@ export class ASRouterUISurface extends React.PureComponent {
UISurface="NEWTAB_OVERLAY"
onAction={ASRouterUtils.executeAction}
onDoneButton={this.clearBundle(this.state.bundle.bundle)}
getNextMessage={ASRouterUtils.getNextMessage}
sendUserActionTelemetry={this.sendUserActionTelemetry} />);
}
@ -270,10 +266,10 @@ export class ASRouterUISurface extends React.PureComponent {
const {message, bundle} = this.state;
if (!message.id && !bundle.template) { return null; }
return (
<div>
<React.Fragment>
{this.renderPreviewBanner()}
{bundle.template === "onboarding" ? this.renderOnboarding() : this.renderSnippets()}
</div>
</React.Fragment>
);
}
}
@ -287,7 +283,14 @@ export class ASRouterContent {
}
_mount() {
this.containerElement = global.document.getElementById("snippets-container");
this.containerElement = global.document.getElementById(ASR_CONTAINER_ID);
if (!this.containerElement) {
this.containerElement = global.document.createElement("div");
this.containerElement.id = ASR_CONTAINER_ID;
this.containerElement.style.zIndex = 1;
global.document.body.appendChild(this.containerElement);
}
ReactDOM.render(<ASRouterUISurface />, this.containerElement);
}

View File

@ -100,8 +100,10 @@ Name | Type | Example value | Description
`providerCohorts` | `Object` | `{onboarding: "hello"}` | Cohorts defined for all providers
`previousSessionEnd` | `Number` | `1536325802800` | Timestamp in milliseconds of previously closed session
`totalBookmarksCount` | `Number` | `8` | Total number of bookmarks
`firefoxVersion` | `Number` | `64` | The major Firefox version of the browser
`firefoxVersion` | `Number` | `64` | The major Firefox version of the browser
`region` | `String` | `US` | Country code retrieved from `location.services.mozilla.com` can be `""` if request did not finish, encountered an error
`locale` | `String` | `en-US` | Locale of the browser
`localeLanguageCode` | `String` | `en` | Locale language code (without country code) of the browser
#### addonsInfo Example
```javascript

View File

@ -16,7 +16,7 @@ export class ASRouterAdmin extends React.PureComponent {
}
componentWillMount() {
const endpoint = ASRouterUtils.getEndpoint();
const endpoint = ASRouterUtils.getPreviewEndpoint();
ASRouterUtils.sendMessage({type: "ADMIN_CONNECT_STATE", data: {endpoint}});
ASRouterUtils.addListener(this.onMessage);
}
@ -51,6 +51,10 @@ export class ASRouterAdmin extends React.PureComponent {
return () => ASRouterUtils.overrideMessage(id);
}
expireCache() {
ASRouterUtils.sendMessage({type: "EXPIRE_QUERY_CACHE"});
}
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.messageBlockList.includes(msg.id);
@ -113,7 +117,8 @@ export class ASRouterAdmin extends React.PureComponent {
render() {
return (<div className="asrouter-admin outer-wrapper">
<h1>AS Router Admin</h1>
<button className="button primary" onClick={ASRouterUtils.getNextMessage}>Refresh Current Message</button>
<h2>Targeting Utilities</h2>
<button className="button" onClick={this.expireCache}>Expire Cache</button> (This expires the cache in ASR Targeting for bookmarks and top sites)
<h2>Message Providers</h2>
{this.state.providers ? this.renderProviders() : null}
<h2>Messages</h2>

View File

@ -31,6 +31,8 @@ export class _StartupOverlay extends React.PureComponent {
if (response.status === 200) {
const {flowId, flowBeginTime} = await response.json();
this.setState({flowId, flowBeginTime});
} else {
this.props.dispatch(ac.OnlyToMain({type: at.TELEMETRY_UNDESIRED_EVENT, data: {event: "FXA_METRICS_FETCH_ERROR", value: response.status}}));
}
} catch (error) {
this.props.dispatch(ac.OnlyToMain({type: at.TELEMETRY_UNDESIRED_EVENT, data: {event: "FXA_METRICS_ERROR"}}));
@ -69,15 +71,24 @@ export class _StartupOverlay extends React.PureComponent {
}
onSubmit() {
this.props.dispatch(ac.UserEvent({event: "SUBMIT_EMAIL"}));
this.props.dispatch(ac.UserEvent({event: "SUBMIT_EMAIL", ...this._getFormInfo()}));
window.addEventListener("visibilitychange", this.removeOverlay);
}
clickSkip() {
this.props.dispatch(ac.UserEvent({event: "SKIPPED_SIGNIN"}));
this.props.dispatch(ac.UserEvent({event: "SKIPPED_SIGNIN", ...this._getFormInfo()}));
this.removeOverlay();
}
/**
* Report to telemetry additional information about the form submission.
*/
_getFormInfo() {
const value = {has_flow_params: this.state.flowId.length > 0};
return {value};
}
onInputInvalid(e) {
let error = e.target.previousSibling;
error.classList.add("active");

View File

@ -964,6 +964,7 @@ var _extends = Object.assign || function (target) { for (var i = 1; i < argument
const INCOMING_MESSAGE_NAME = "ASRouter:parent-to-child";
const OUTGOING_MESSAGE_NAME = "ASRouter:child-to-parent";
const ASR_CONTAINER_ID = "asr-newtab-container";
const ASRouterUtils = {
addListener(listener) {
@ -993,9 +994,6 @@ const ASRouterUtils = {
unblockBundle(bundle) {
ASRouterUtils.sendMessage({ type: "UNBLOCK_BUNDLE", data: { bundle } });
},
getNextMessage() {
ASRouterUtils.sendMessage({ type: "GET_NEXT_MESSAGE" });
},
overrideMessage(id) {
ASRouterUtils.sendMessage({ type: "OVERRIDE_MESSAGE", data: { id } });
},
@ -1003,7 +1001,7 @@ const ASRouterUtils = {
const payload = common_Actions_jsm__WEBPACK_IMPORTED_MODULE_1__["actionCreators"].ASRouterUserEvent(ping);
global.RPMSendAsyncMessage(content_src_lib_init_store__WEBPACK_IMPORTED_MODULE_2__["OUTGOING_MESSAGE_NAME"], payload);
},
getEndpoint() {
getPreviewEndpoint() {
if (window.location.href.includes("endpoint")) {
const params = new URLSearchParams(window.location.href.slice(window.location.href.indexOf("endpoint")));
try {
@ -1159,14 +1157,14 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
}
componentWillMount() {
const endpoint = ASRouterUtils.getEndpoint();
const endpoint = ASRouterUtils.getPreviewEndpoint();
ASRouterUtils.addListener(this.onMessageFromParent);
// If we are loading about:welcome we want to trigger the onboarding messages
if (this.props.document.location.href === "about:welcome") {
ASRouterUtils.sendMessage({ type: "TRIGGER", data: { trigger: { id: "firstRun" } } });
} else {
ASRouterUtils.sendMessage({ type: "CONNECT_UI_REQUEST", data: { endpoint } });
ASRouterUtils.sendMessage({ type: "SNIPPETS_REQUEST", data: { endpoint } });
}
}
@ -1192,7 +1190,6 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
links: this.state.message.content.links,
sendClick: this.sendClick }),
UISurface: "NEWTAB_FOOTER_BAR",
getNextMessage: ASRouterUtils.getNextMessage,
onBlock: this.onBlockById(this.state.message.id),
onAction: ASRouterUtils.executeAction,
sendUserActionTelemetry: this.sendUserActionTelemetry }))
@ -1205,7 +1202,6 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
UISurface: "NEWTAB_OVERLAY",
onAction: ASRouterUtils.executeAction,
onDoneButton: this.clearBundle(this.state.bundle.bundle),
getNextMessage: ASRouterUtils.getNextMessage,
sendUserActionTelemetry: this.sendUserActionTelemetry }));
}
@ -1232,7 +1228,7 @@ class ASRouterUISurface extends react__WEBPACK_IMPORTED_MODULE_6___default.a.Pur
return null;
}
return react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(
"div",
react__WEBPACK_IMPORTED_MODULE_6___default.a.Fragment,
null,
this.renderPreviewBanner(),
bundle.template === "onboarding" ? this.renderOnboarding() : this.renderSnippets()
@ -1249,7 +1245,14 @@ class ASRouterContent {
}
_mount() {
this.containerElement = global.document.getElementById("snippets-container");
this.containerElement = global.document.getElementById(ASR_CONTAINER_ID);
if (!this.containerElement) {
this.containerElement = global.document.createElement("div");
this.containerElement.id = ASR_CONTAINER_ID;
this.containerElement.style.zIndex = 1;
global.document.body.appendChild(this.containerElement);
}
react_dom__WEBPACK_IMPORTED_MODULE_7___default.a.render(react__WEBPACK_IMPORTED_MODULE_6___default.a.createElement(ASRouterUISurface, null), this.containerElement);
}
@ -1784,7 +1787,7 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
}
componentWillMount() {
const endpoint = _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].getEndpoint();
const endpoint = _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].getPreviewEndpoint();
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "ADMIN_CONNECT_STATE", data: { endpoint } });
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].addListener(this.onMessage);
}
@ -1819,6 +1822,10 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
return () => _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].overrideMessage(id);
}
expireCache() {
_asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].sendMessage({ type: "EXPIRE_QUERY_CACHE" });
}
renderMessageItem(msg) {
const isCurrent = msg.id === this.state.lastMessageId;
const isBlocked = this.state.messageBlockList.includes(msg.id);
@ -1970,10 +1977,16 @@ class ASRouterAdmin extends react__WEBPACK_IMPORTED_MODULE_1___default.a.PureCom
"AS Router Admin"
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"button",
{ className: "button primary", onClick: _asrouter_asrouter_content__WEBPACK_IMPORTED_MODULE_0__["ASRouterUtils"].getNextMessage },
"Refresh Current Message"
"h2",
null,
"Targeting Utilities"
),
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"button",
{ className: "button", onClick: this.expireCache },
"Expire Cache"
),
" (This expires the cache in ASR Targeting for bookmarks and top sites)",
react__WEBPACK_IMPORTED_MODULE_1___default.a.createElement(
"h2",
null,
@ -5144,6 +5157,8 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
if (response.status === 200) {
const { flowId, flowBeginTime } = yield response.json();
_this.setState({ flowId, flowBeginTime });
} else {
_this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT, data: { event: "FXA_METRICS_FETCH_ERROR", value: response.status } }));
}
} catch (error) {
_this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].OnlyToMain({ type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].TELEMETRY_UNDESIRED_EVENT, data: { event: "FXA_METRICS_ERROR" } }));
@ -5183,15 +5198,24 @@ class _StartupOverlay extends react__WEBPACK_IMPORTED_MODULE_3___default.a.PureC
}
onSubmit() {
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({ event: "SUBMIT_EMAIL" }));
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({ event: "SUBMIT_EMAIL" }, this._getFormInfo())));
window.addEventListener("visibilitychange", this.removeOverlay);
}
clickSkip() {
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent({ event: "SKIPPED_SIGNIN" }));
this.props.dispatch(common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].UserEvent(Object.assign({ event: "SKIPPED_SIGNIN" }, this._getFormInfo())));
this.removeOverlay();
}
/**
* Report to telemetry additional information about the form submission.
*/
_getFormInfo() {
const value = { has_flow_params: this.state.flowId.length > 0 };
return { value };
}
onInputInvalid(e) {
let error = e.target.previousSibling;
error.classList.add("active");

File diff suppressed because one or more lines are too long

View File

@ -235,7 +235,8 @@ and losing focus. | :one:
| `icon_type` | [Optional] ("tippytop", "rich_icon", "screenshot_with_icon", "screenshot", "no_image") | :one:
| `region` | [Optional] A string maps to pref "browser.search.region", which is essentially the two letter ISO 3166-1 country code populated by the Firefox search service. Note that: 1). it reports "OTHER" for those regions with smaller Firefox user base (less than 10000) so that users cannot be uniquely identified; 2). it reports "UNSET" if this pref is missing; 3). it reports "EMPTY" if the value of this pref is an empty string. | :one:
| `profile_creation_date` | [Optional] An integer to record the age of the Firefox profile as the total number of days since the UNIX epoch. | :one:
|`message_id` | [required] A string identifier of the message in Activity Stream Router. | :one:
| `message_id` | [required] A string identifier of the message in Activity Stream Router. | :one:
| `has_flow_params` | [required] One of [true, false]. A boolean identifier that indicates if Firefox Accounts flow parameters are set or unset. | :one:
**Where:**

View File

@ -342,6 +342,47 @@ A user event ping includes some basic metadata (tab id, addon version, etc.) as
}
```
### Onboarding user events on about:welcome
#### Form Submit Events
```js
{
"event": ["SUBMIT_EMAIL" | "SKIPPED_SIGNIN"],
"value": {
"has_flow_params": false,
}
// Basic metadata
"action": "activity_stream_event",
"page": "about:welcome",
"client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
"session_id": "005deed0-e3e4-4c02-a041-17405fd703f6",
"addon_version": "20180710100040",
"locale": "en-US",
"user_prefs": 7
}
```
#### Firefox Accounts Metrics flow errors
```js
{
"event": ["FXA_METRICS_FETCH_ERROR" | "FXA_METRICS_ERROR"],
"value": 500, // Only FXA_METRICS_FETCH_ERROR provides this value, this value is any valid HTTP status code except 200.
// Basic metadata
"action": "activity_stream_event",
"page": "about:welcome",
"client_id": "26288a14-5cc4-d14f-ae0a-bb01ef45be9c",
"session_id": "005deed0-e3e4-4c02-a041-17405fd703f6",
"addon_version": "20180710100040",
"locale": "en-US",
"user_prefs": 7
}
```
## Session end pings
When a session ends, the browser will send a `"activity_stream_session"` ping to our metrics servers. This ping contains the length of the session, a unique reason for why the session ended, and some additional metadata.

View File

@ -20,6 +20,8 @@ ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
"resource://activity-stream/lib/ASRouterPreferences.jsm");
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting",
"resource://activity-stream/lib/ASRouterTargeting.jsm");
ChromeUtils.defineModuleGetter(this, "QueryCache",
"resource://activity-stream/lib/ASRouterTargeting.jsm");
ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
"resource://activity-stream/lib/ASRouterTriggerListeners.jsm");
@ -242,6 +244,17 @@ const MessageLoaderUtils = {
this.MessageLoaderUtils = MessageLoaderUtils;
/**
* hasLegacyOnboardingConflict - Checks if we need to turn off snippets because of
* legacy onboarding using the same UI space
*
* @param {Provider} provider
* @returns {boolean} Is there a conflict with legacy onboarding?
*/
function hasLegacyOnboardingConflict(provider) {
return provider.id === "snippets" && !Services.prefs.getBoolPref(ONBOARDING_FINISHED_PREF, false);
}
/**
* @class _ASRouter - Keeps track of all messages, UI surfaces, and
* handles blocking, rotation, etc. Inspecting ASRouter.state will
@ -293,6 +306,14 @@ class _ASRouter {
}
}
// This will be removed when legacy onboarding is removed.
async observe(aSubject, aTopic, aPrefName) {
if (aPrefName === ONBOARDING_FINISHED_PREF) {
this._updateMessageProviders();
await this.loadMessagesFromAllProviders();
}
}
// Update message providers and fetch new messages on pref change
async onPrefChange() {
this._updateMessageProviders();
@ -315,10 +336,17 @@ class _ASRouter {
// Fetch and decode the message provider pref JSON, and update the message providers
_updateMessageProviders() {
const previousProviders = this.state.providers;
const providers = [
// If we have added a `preview` provider, hold onto it
...this.state.providers.filter(p => p.id === "preview"),
...ASRouterPreferences.providers.filter(p => p.enabled),
...previousProviders.filter(p => p.id === "preview"),
// The provider should be enabled and not have a user preference set to false
...ASRouterPreferences.providers.filter(p => (
p.enabled &&
ASRouterPreferences.getUserPreference(p.id) !== false) &&
// sorry this is crappy. will remove soon
!hasLegacyOnboardingConflict(p)
),
].map(_provider => {
// make a copy so we don't modify the source of the pref
const provider = {..._provider};
@ -339,6 +367,14 @@ class _ASRouter {
});
const providerIDs = providers.map(p => p.id);
// Clear old messages for providers that are no longer enabled
for (const prevProvider of previousProviders) {
if (!providerIDs.includes(prevProvider.id)) {
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "CLEAR_PROVIDER", data: {id: prevProvider.id}});
}
}
this.setState(prevState => ({
providers,
// Clear any messages from removed providers
@ -436,6 +472,8 @@ class _ASRouter {
this.dispatchToAS = dispatchToAS;
this.dispatch = this.dispatch.bind(this);
// For watching legacy onboarding. To be removed when legacy onboarding is gone.
Services.prefs.addObserver(ONBOARDING_FINISHED_PREF, this);
ASRouterPreferences.init();
ASRouterPreferences.addListener(this.onPrefChange);
@ -467,6 +505,8 @@ class _ASRouter {
this.overrideOrEnableLegacyOnboarding();
// For watching legacy onboarding. To be removed when legacy onboarding is gone.
Services.prefs.removeObserver(ONBOARDING_FINISHED_PREF, this);
ASRouterPreferences.removeListener(this.onPrefChange);
ASRouterPreferences.uninit();
@ -494,10 +534,15 @@ class _ASRouter {
_onStateChanged(state) {
if (ASRouterPreferences.devtoolsEnabled) {
this.messageChannel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: this.state});
this._updateAdminState();
}
}
_updateAdminState(target) {
const channel = target || this.messageChannel;
channel.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: this.state});
}
_handleTargetingError(type, error, message) {
Cu.reportError(error);
if (this.dispatchToAS) {
@ -907,8 +952,7 @@ class _ASRouter {
await this.handleUserAction({data: action.data, target});
}
break;
case "CONNECT_UI_REQUEST":
case "GET_NEXT_MESSAGE":
case "SNIPPETS_REQUEST":
case "TRIGGER":
// Wait for our initial message loading to be done before responding to any UI requests
await this.waitForInitialized;
@ -965,7 +1009,7 @@ class _ASRouter {
this._addPreviewEndpoint(action.data.endpoint.url, target.portID);
await this.loadMessagesFromAllProviders();
} else {
target.sendAsyncMessage(OUTGOING_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: this.state});
this._updateAdminState(target);
}
break;
case "IMPRESSION":
@ -976,6 +1020,9 @@ class _ASRouter {
this.dispatchToAS(ac.ASRouterUserEvent(action.data));
}
break;
case "EXPIRE_QUERY_CACHE":
QueryCache.expireAll();
break;
}
}
}

View File

@ -16,6 +16,8 @@ const DEFAULT_STATE = {
_devtoolsPref: DEVTOOLS_PREF,
};
const USER_PREFERENCES = {snippets: "browser.newtabpage.activity-stream.feeds.snippets"};
class _ASRouterPreferences {
constructor() {
Object.assign(this, DEFAULT_STATE);
@ -71,6 +73,13 @@ class _ASRouterPreferences {
this._callbacks.forEach(cb => cb(aPrefName));
}
getUserPreference(providerId) {
if (!USER_PREFERENCES[providerId]) {
return null;
}
return Services.prefs.getBoolPref(USER_PREFERENCES[providerId], true);
}
addListener(callback) {
this._callbacks.add(callback);
}
@ -85,6 +94,9 @@ class _ASRouterPreferences {
}
Services.prefs.addObserver(this._providerPref, this);
Services.prefs.addObserver(this._devtoolsPref, this);
for (const id of Object.keys(USER_PREFERENCES)) {
Services.prefs.addObserver(USER_PREFERENCES[id], this);
}
this._initialized = true;
}
@ -92,6 +104,9 @@ class _ASRouterPreferences {
if (this._initialized) {
Services.prefs.removeObserver(this._providerPref, this);
Services.prefs.removeObserver(this._devtoolsPref, this);
for (const id of Object.keys(USER_PREFERENCES)) {
Services.prefs.removeObserver(USER_PREFERENCES[id], this);
}
}
Object.assign(this, DEFAULT_STATE);
this._callbacks.clear();

View File

@ -27,8 +27,14 @@ const FRECENT_SITES_IGNORE_BLOCKED = false;
const FRECENT_SITES_NUM_ITEMS = 25;
const FRECENT_SITES_MIN_FRECENCY = 100;
/**
* CachedTargetingGetter
* @param property {string} Name of the method called on ActivityStreamProvider
* @param options {{}?} Options object passsed to ActivityStreamProvider method
* @param updateInterval {number?} Update interval for query. Defaults to FRECENT_SITES_UPDATE_INTERVAL
*/
function CachedTargetingGetter(property, options = null, updateInterval = FRECENT_SITES_UPDATE_INTERVAL) {
const targetingGetter = {
return {
_lastUpdated: 0,
_value: null,
// For testing
@ -36,39 +42,44 @@ function CachedTargetingGetter(property, options = null, updateInterval = FRECEN
this._lastUpdated = 0;
this._value = null;
},
};
Object.defineProperty(targetingGetter, property, {
get: () => new Promise(async (resolve, reject) => {
const now = Date.now();
if (now - targetingGetter._lastUpdated >= updateInterval) {
try {
targetingGetter._value = await asProvider[property](options);
targetingGetter._lastUpdated = now;
} catch (e) {
Cu.reportError(e);
reject(e);
get() {
return new Promise(async (resolve, reject) => {
const now = Date.now();
if (now - this._lastUpdated >= updateInterval) {
try {
this._value = await asProvider[property](options);
this._lastUpdated = now;
} catch (e) {
Cu.reportError(e);
reject(e);
}
}
}
resolve(targetingGetter._value);
}),
});
return targetingGetter;
resolve(this._value);
});
},
};
}
const TopFrecentSitesCache = new CachedTargetingGetter(
"getTopFrecentSites",
{
ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
numItems: FRECENT_SITES_NUM_ITEMS,
topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
onePerDomain: true,
includeFavicon: false,
}
);
const TotalBookmarksCountCache = new CachedTargetingGetter("getTotalBookmarksCount");
const QueryCache = {
expireAll() {
Object.keys(this.queries).forEach(query => {
this.queries[query].expire();
});
},
queries: {
TopFrecentSites: new CachedTargetingGetter(
"getTopFrecentSites",
{
ignoreBlocked: FRECENT_SITES_IGNORE_BLOCKED,
numItems: FRECENT_SITES_NUM_ITEMS,
topsiteFrecency: FRECENT_SITES_MIN_FRECENCY,
onePerDomain: true,
includeFavicon: false,
}
),
TotalBookmarksCount: new CachedTargetingGetter("getTotalBookmarksCount"),
},
};
/**
* sortMessagesByWeightedRank
@ -97,6 +108,12 @@ function sortMessagesByWeightedRank(messages) {
}
const TargetingGetters = {
get locale() {
return Services.locale.appLocaleAsLangTag;
},
get localeLanguageCode() {
return Services.locale.appLocaleAsLangTag && Services.locale.appLocaleAsLangTag.substr(0, 2);
},
get browserSettings() {
const {settings} = TelemetryEnvironment.currentEnvironment;
return {
@ -173,7 +190,7 @@ const TargetingGetters = {
return Services.prefs.getIntPref("devtools.selfxss.count");
},
get topFrecentSites() {
return TopFrecentSitesCache.getTopFrecentSites.then(sites => sites.map(site => (
return QueryCache.queries.TopFrecentSites.get().then(sites => sites.map(site => (
{
url: site.url,
host: (new URL(site.url)).hostname,
@ -194,7 +211,7 @@ const TargetingGetters = {
}, {});
},
get totalBookmarksCount() {
return TotalBookmarksCountCache.getTotalBookmarksCount;
return QueryCache.queries.TotalBookmarksCount.get();
},
get firefoxVersion() {
return parseInt(AppConstants.MOZ_APP_VERSION.match(/\d+/), 10);
@ -289,7 +306,6 @@ this.ASRouterTargeting = {
};
// Export for testing
this.TopFrecentSitesCache = TopFrecentSitesCache;
this.TotalBookmarksCountCache = TotalBookmarksCountCache;
this.QueryCache = QueryCache;
this.CachedTargetingGetter = CachedTargetingGetter;
this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "TopFrecentSitesCache", "TotalBookmarksCountCache", "CachedTargetingGetter"];
this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "QueryCache", "CachedTargetingGetter"];

View File

@ -82,6 +82,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 1},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "one_per_day_amazon") &&
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${AMAZON_ASSISTANT_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -124,6 +125,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 3},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "three_per_day_amazon") &&
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(AMAZON_ASSISTANT_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${AMAZON_ASSISTANT_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -166,6 +168,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 1},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr in ["one_per_day", "nightly"]) &&
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${FACEBOOK_CONTAINER_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -208,6 +211,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 3},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "three_per_day") &&
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(FACEBOOK_CONTAINER_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${FACEBOOK_CONTAINER_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -250,6 +254,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 1},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr in ["one_per_day", "nightly"]) &&
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${GOOGLE_TRANSLATE_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -292,6 +297,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 3},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "three_per_day") &&
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(GOOGLE_TRANSLATE_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${GOOGLE_TRANSLATE_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -334,6 +340,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 1},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr in ["one_per_day", "nightly"]) &&
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${YOUTUBE_ENHANCE_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -376,6 +383,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 3},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "three_per_day") &&
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(YOUTUBE_ENHANCE_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${YOUTUBE_ENHANCE_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -418,6 +426,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 1},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr in ["one_per_day", "nightly"]) &&
(${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[.frecency >= ${WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -460,6 +469,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 3},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "three_per_day") &&
(${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[.frecency >= ${WIKIPEDIA_CONTEXT_MENU_SEARCH_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -502,6 +512,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 1},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr in ["one_per_day", "nightly"]) &&
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${REDDIT_ENHANCEMENT_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,
@ -544,6 +555,7 @@ const CFR_MESSAGES = [
},
frequency: {lifetime: 3},
targeting: `
localeLanguageCode == "en" &&
(providerCohorts.cfr == "three_per_day") &&
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.existing_addons)} intersect addonsInfo.addons|keys)|length == 0 &&
(${JSON.stringify(REDDIT_ENHANCEMENT_PARAMS.open_urls)} intersect topFrecentSites[.frecency >= ${REDDIT_ENHANCEMENT_PARAMS.min_frecency}]|mapToProperty('host'))|length > 0`,

View File

@ -11,6 +11,8 @@ ChromeUtils.import("resource://gre/modules/XPCOMUtils.jsm");
const {actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});
const {Prefs} = ChromeUtils.import("resource://activity-stream/lib/ActivityStreamPrefs.jsm", {});
ChromeUtils.defineModuleGetter(this, "ASRouterPreferences",
"resource://activity-stream/lib/ASRouterPreferences.jsm");
ChromeUtils.defineModuleGetter(this, "perfService",
"resource://activity-stream/common/PerfService.jsm");
ChromeUtils.defineModuleGetter(this, "PingCentre",
@ -41,7 +43,6 @@ const USER_PREFS_ENCODING = {
const PREF_IMPRESSION_ID = "impressionId";
const TELEMETRY_PREF = "telemetry";
const EVENTS_TELEMETRY_PREF = "telemetry.ut.events";
const ROUTER_MESSAGE_PROVIDER_PREF = "asrouter.messageProviders";
this.TelemetryFeed = class TelemetryFeed {
constructor(options) {
@ -55,8 +56,6 @@ this.TelemetryFeed = class TelemetryFeed {
this._prefs.observe(TELEMETRY_PREF, this._onTelemetryPrefChange);
this._onEventsTelemetryPrefChange = this._onEventsTelemetryPrefChange.bind(this);
this._prefs.observe(EVENTS_TELEMETRY_PREF, this._onEventsTelemetryPrefChange);
this._onRouterMessageProviderChange = this._onRouterMessageProviderChange.bind(this);
this._prefs.observe(ROUTER_MESSAGE_PROVIDER_PREF, this._onRouterMessageProviderChange);
}
init() {
@ -119,28 +118,6 @@ this.TelemetryFeed = class TelemetryFeed {
this.eventTelemetryEnabled = prefVal;
}
/**
* Check the CFR experiment cohort information by parsing the pref string of
* AS router message provider. The experiment cohort can be identified by the
* `cohort` field in the "cfr" provider.
*/
_parseCFRCohort(pref) {
try {
for (let provider of JSON.parse(pref)) {
if (provider.id === "cfr" && provider.enabled && provider.cohort) {
return true;
}
}
} catch (e) {
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
}
return false;
}
_onRouterMessageProviderChange(prefVal) {
this._isInCFRCohort = this._parseCFRCohort(prefVal);
}
/**
* Lazily initialize PingCentre for Activity Stream to send pings
*/
@ -190,14 +167,15 @@ this.TelemetryFeed = class TelemetryFeed {
}
/**
* Lazily parse the AS router pref to check if it is in the CFR experiment cohort
* Check if it is in the CFR experiment cohort. ASRouterPreferences lazily parses AS router pref.
*/
get isInCFRCohort() {
if (this._isInCFRCohort === undefined) {
const pref = this._prefs.get(ROUTER_MESSAGE_PROVIDER_PREF);
this._isInCFRCohort = this._parseCFRCohort(pref);
for (let provider of ASRouterPreferences.providers) {
if (provider.id === "cfr" && provider.enabled && provider.cohort) {
return true;
}
}
return this._isInCFRCohort;
return false;
}
/**
@ -585,7 +563,6 @@ this.TelemetryFeed = class TelemetryFeed {
try {
this._prefs.ignore(TELEMETRY_PREF, this._onTelemetryPrefChange);
this._prefs.ignore(EVENTS_TELEMETRY_PREF, this._onEventsTelemetryPrefChange);
this._prefs.ignore(ROUTER_MESSAGE_PROVIDER_PREF, this._onRouterMessageProviderChange);
} catch (e) {
Cu.reportError(e);
}
@ -599,5 +576,4 @@ const EXPORTED_SYMBOLS = [
"PREF_IMPRESSION_ID",
"TELEMETRY_PREF",
"EVENTS_TELEMETRY_PREF",
"ROUTER_MESSAGE_PROVIDER_PREF",
];

View File

@ -94,7 +94,7 @@ prefs_home_description=Pilih konten yang ingin Anda tampilkan dalam Beranda Fire
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
prefs_section_rows_option={num} baris;{num} baris
prefs_section_rows_option={num} baris
prefs_search_header=Pencarian Web
prefs_topsites_description=Situs yang sering Anda kunjungi
prefs_topstories_description2=Konten bermutu dari seluruh web, khusus untuk Anda

View File

@ -0,0 +1,118 @@
newtab_page_title=Nýr flipi
header_highlights=Úrdrættir
# LOCALIZATION NOTE(header_recommended_by): This is followed by the name
# of the corresponding content provider.
# LOCALIZATION NOTE(context_menu_button_sr): This is for screen readers when
# the context menu button is focused/active. Title is the label or hostname of
# the site.
# LOCALIZATION NOTE(section_context_menu_button_sr): This is for screen readers when
# the section edit context menu button is focused/active.
# LOCALIZATION NOTE (type_label_*): These labels are associated to pages to give
# context on how the element is related to the user, e.g. type indicates that
# the page is bookmarked, or is currently open on another device
type_label_visited=Heimsótt
type_label_bookmarked=Búið að bókamerkja
type_label_recommended=Vinsælt
type_label_pocket=Vistað í Pocket
type_label_downloaded=Niðurhalað
# LOCALIZATION NOTE (menu_action_*): These strings are displayed in a context
# menu and are meant as a call to action for a given page.
# LOCALIZATION NOTE (menu_action_bookmark): Bookmark is a verb, as in "Add to
# bookmarks"
menu_action_dismiss=Hafna
menu_action_delete=Eyða úr ferli
# LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in
# the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
# page from history.
confirm_history_delete_notice_p2=Ekki er ekki hægt að bakfæra þessa aðgerð.
menu_action_save_to_pocket=Vista í Pocket
# LOCALIZATION NOTE (menu_action_show_file_*): These are platform specific strings
# found in the context menu of an item that has been downloaded. The intention behind
# "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=Sýna í Finder
menu_action_show_file_windows=Opna möppu
menu_action_show_file_linux=Opna möppu
menu_action_show_file_default=Sýna skrá
menu_action_open_file=Opna skrá
# LOCALIZATION NOTE (menu_action_copy_download_link, menu_action_go_to_download_page):
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
# link that belongs to this downloaded item"
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
# search button.
# LOCALIZATION NOTE (search_header): Displayed at the top of the panel
# showing search suggestions. {search_engine_name} is replaced with the name of
# the current default search engine. e.g. 'Google Search'
# LOCALIZATION NOTE (search_web_placeholder): This is shown in the searchbox when
# the user hasn't typed anything yet.
# LOCALIZATION NOTE (section_disclaimer_topstories): This is shown below
# the topstories section title to provide additional information about
# how the stories are selected.
# LOCALIZATION NOTE (section_disclaimer_topstories_buttontext): The text of
# the button used to acknowledge, and hide this disclaimer in the future.
# LOCALIZATION NOTE (prefs_*, settings_*): These are shown in about:preferences
# for a "Firefox Home" section. "Firefox" should be treated as a brand and kept
# in English, while "Home" should be localized matching the about:preferences
# sidebar mozilla-central string for the panel that has preferences related to
# what is shown for the homepage, new windows, and new tabs.
# LOCALIZATION NOTE (prefs_section_rows_option): This is a semi-colon list of
# plural forms used in a drop down of multiple row options (1 row, 2 rows).
# See: http://developer.mozilla.org/en/docs/Localization_and_Plurals
# LOCALIZATION NOTE(settings_pane_snippets_header): For the "Snippets" feature
# traditionally on about:home. Alternative translation options: "Small Note" or
# something that expresses the idea of "a small message, shortened from
# something else, and non-essential but also not entirely trivial and useless."
# LOCALIZATION NOTE (edit_topsites_*): This is shown in the Edit Top Sites modal
# dialog.
# LOCALIZATION NOTE (topsites_form_*): This is shown in the New/Edit Topsite modal.
# LOCALIZATION NOTE (topsites_form_*_button): These are verbs/actions.
# 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.
# LOCALIZATION NOTE (pocket_read_even_more): This is shown as a link at the
# end of the list of popular topic links.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
# in the space that would have shown a few stories, this is shown instead.
# {provider} is replaced by the name of the content provider for this section.
# LOCALIZATION NOTE (manual_migration_explanation2): This message is shown to encourage users to
# import their browser profile from another browser they might be using.
# LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the
# process of importing another browsers profile into Firefox.
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process
# of importing another browsers profile profile into Firefox.
# LOCALIZATION NOTE (error_fallback_default_*): This message and suggested
# action link are shown in each section of UI that fails to render
# 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.
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
# 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.
# 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.

View File

@ -145,6 +145,7 @@ pocket_read_more=Populære emne:
pocket_read_even_more=Vis fleire saker
pocket_learn_more=Les meir
pocket_cta_button=Last ned Pocket
highlights_empty_state=Begynn å surfe, og vi vil vise deg nokre av dei beste artiklane, videoane og andre sider du nyleg har besøkt eller bokmerka her.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,

View File

@ -144,6 +144,8 @@ pocket_read_more=Tèmas populars:
# end of the list of popular topic links.
pocket_read_even_more=Veire mai darticles
pocket_learn_more=Ne saber mai
highlights_empty_state=Començatz de navegar e aquí vos mostrarem los melhors articles, vidèos e autras paginas quavètz visitadas o apondudas als marcapaginas.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
# in the space that would have shown a few stories, this is shown instead.

View File

@ -82,9 +82,15 @@ topsites_form_save_button=Zachowaj
topsites_form_cancel_button=Anuluj
topsites_form_url_validation=Wymagany jest prawidłowy adres URL
topsites_form_image_validation=Wczytanie obrazu nie powiodło się. Spróbuj innego adresu.
pocket_read_more=Popularne treści:
pocket_read_even_more=Pokaż więcej artykułów
pocket_more_reccommendations=Więcej polecanych
pocket_learn_more=Więcej informacji
pocket_cta_button=Pobierz Pocket
pocket_cta_text=Zachowuj historie w Pocket, aby wrócić później do ich lektury.
pocket_description=Odkrywaj wysokiej jakości treści dzięki serwisowi Pocket, który jest teraz częścią Mozilli.
highlights_empty_state=Zacznij przeglądać Internet, a pojawią się tutaj świetne artykuły, filmy oraz inne ostatnio odwiedzane strony i dodane zakładki.
topstories_empty_state=To na razie wszystko. {provider} później będzie mieć więcej popularnych artykułów. Nie możesz się doczekać? Wybierz popularny temat, aby znaleźć więcej treści z całego Internetu.
manual_migration_explanation2=Wypróbuj program Firefox z zakładkami, historią i hasłami z innej przeglądarki.

View File

@ -144,6 +144,10 @@ pocket_read_more=Tópicos populares:
# end of the list of popular topic links.
pocket_read_even_more=Ver mais histórias
pocket_more_reccommendations=Mais recomendações
pocket_learn_more=Saiba mais
pocket_cta_text=Salve as histórias que você gosta no Pocket e abasteça sua mente com leituras fascinantes.
highlights_empty_state=Comece a navegar e nós mostraremos aqui alguns ótimos artigos, vídeos e outras páginas que você favoritou ou visitou recentemente.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,
# in the space that would have shown a few stories, this is shown instead.

View File

@ -147,6 +147,7 @@ pocket_read_even_more=Zobraziť ďalšie príbehy
pocket_more_reccommendations=Ďalšie odporúčania
pocket_learn_more=Ďalšie informácie
pocket_cta_button=Získajte Pocket
pocket_cta_text=Ukladajte si články do služby Pocket a užívajte si skvelé čítanie.
highlights_empty_state=Začnite s prehliadaním a my vám na tomto mieste ukážeme skvelé články, videá a ostatné stránky, ktoré ste nedávno navštívili alebo pridali medzi záložky.
# LOCALIZATION NOTE (topstories_empty_state): When there are no recommendations,

View File

@ -46,7 +46,9 @@ menu_action_delete_pocket=Pocket سے جزف کریں
# found in the context menu of an item that has been downloaded. The intention behind
# "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=تلاش کار میں دکھائیں
menu_action_show_file_windows=حامل پوشہ کھولیں
menu_action_show_file_linux=مشتمل فولڈر کھولیں
menu_action_show_file_default=مسل دکھائیں
menu_action_open_file=مسل کھولیں
@ -54,6 +56,7 @@ menu_action_open_file=مسل کھولیں
# "Download" here, in both cases, is not a verb, it is a noun. As in, "Copy the
# link that belongs to this downloaded item"
menu_action_copy_download_link=ڈاؤن لوڈ ربط نقل کریں
menu_action_go_to_download_page=ڈاؤن لوڈ صفحہ پر جائیں
menu_action_remove_download=سابقات سے ہٹائیں
# LOCALIZATION NOTE (search_button): This is screenreader only text for the
@ -165,6 +168,7 @@ firstrun_form_header=اپنی ای میل داخل کریں
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.

View File

@ -40,7 +40,7 @@ window.gActivityStreamStrings = {
"section_disclaimer_topstories_buttontext": "Oke, paham",
"prefs_home_header": "Konten Beranda Firefox",
"prefs_home_description": "Pilih konten yang ingin Anda tampilkan dalam Beranda Firefox.",
"prefs_section_rows_option": "{num} baris;{num} baris",
"prefs_section_rows_option": "{num} baris",
"prefs_search_header": "Pencarian Web",
"prefs_topsites_description": "Situs yang sering Anda kunjungi",
"prefs_topstories_description2": "Konten bermutu dari seluruh web, khusus untuk Anda",

View File

@ -0,0 +1,17 @@
<!doctype html>
<html lang="is" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
<title>Nýr flipi</title>
<link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="root"></div>
<div id="snippets-container">
<div id="snippets"></div>
</div>
</body>
</html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -0,0 +1,110 @@
// Note - this is a generated is file.
window.gActivityStreamStrings = {
"newtab_page_title": "Nýr flipi",
"header_top_sites": "Top Sites",
"header_highlights": "Úrdrættir",
"header_recommended_by": "Recommended by {provider}",
"context_menu_button_sr": "Open context menu for {title}",
"section_context_menu_button_sr": "Open the section context menu",
"type_label_visited": "Heimsótt",
"type_label_bookmarked": "Búið að bókamerkja",
"type_label_recommended": "Vinsælt",
"type_label_pocket": "Vistað í Pocket",
"type_label_downloaded": "Niðurhalað",
"menu_action_bookmark": "Bookmark",
"menu_action_remove_bookmark": "Remove Bookmark",
"menu_action_open_new_window": "Open in a New Window",
"menu_action_open_private_window": "Open in a New Private Window",
"menu_action_dismiss": "Hafna",
"menu_action_delete": "Eyða úr ferli",
"menu_action_pin": "Pin",
"menu_action_unpin": "Unpin",
"confirm_history_delete_p1": "Are you sure you want to delete every instance of this page from your history?",
"confirm_history_delete_notice_p2": "Ekki er ekki hægt að bakfæra þessa aðgerð.",
"menu_action_save_to_pocket": "Vista í Pocket",
"menu_action_delete_pocket": "Delete from Pocket",
"menu_action_archive_pocket": "Archive in Pocket",
"menu_action_show_file_mac_os": "Sýna í Finder",
"menu_action_show_file_windows": "Opna möppu",
"menu_action_show_file_linux": "Opna möppu",
"menu_action_show_file_default": "Sýna skrá",
"menu_action_open_file": "Opna skrá",
"menu_action_copy_download_link": "Copy Download Link",
"menu_action_go_to_download_page": "Go to Download Page",
"menu_action_remove_download": "Remove from History",
"search_button": "Search",
"search_header": "{search_engine_name} Search",
"search_web_placeholder": "Search the Web",
"section_disclaimer_topstories": "The most interesting stories on the web, selected based on what you read. From Pocket, now part of Mozilla.",
"section_disclaimer_topstories_linktext": "Learn how it works.",
"section_disclaimer_topstories_buttontext": "Okay, got it",
"prefs_home_header": "Firefox Home Content",
"prefs_home_description": "Choose what content you want on your Firefox Home screen.",
"prefs_section_rows_option": "{num} row;{num} rows",
"prefs_search_header": "Web Search",
"prefs_topsites_description": "The sites you visit most",
"prefs_topstories_description2": "Great content from around the web, personalized for you",
"prefs_topstories_options_sponsored_label": "Sponsored Stories",
"prefs_topstories_sponsored_learn_more": "Learn more",
"prefs_highlights_description": "A selection of sites that youve saved or visited",
"prefs_highlights_options_visited_label": "Visited Pages",
"prefs_highlights_options_download_label": "Most Recent Download",
"prefs_highlights_options_pocket_label": "Pages Saved to Pocket",
"prefs_snippets_description": "Updates from Mozilla and Firefox",
"settings_pane_button_label": "Customize your New Tab page",
"settings_pane_topsites_header": "Top Sites",
"settings_pane_highlights_header": "Highlights",
"settings_pane_highlights_options_bookmarks": "Bookmarks",
"settings_pane_snippets_header": "Snippets",
"edit_topsites_button_text": "Edit",
"edit_topsites_edit_button": "Edit this site",
"topsites_form_add_header": "New Top Site",
"topsites_form_edit_header": "Edit Top Site",
"topsites_form_title_label": "Title",
"topsites_form_title_placeholder": "Enter a title",
"topsites_form_url_label": "URL",
"topsites_form_image_url_label": "Custom Image URL",
"topsites_form_url_placeholder": "Type or paste a URL",
"topsites_form_use_image_link": "Use a custom image…",
"topsites_form_preview_button": "Preview",
"topsites_form_add_button": "Add",
"topsites_form_save_button": "Save",
"topsites_form_cancel_button": "Cancel",
"topsites_form_url_validation": "Valid URL required",
"topsites_form_image_validation": "Image failed to load. Try a different URL.",
"pocket_read_more": "Popular Topics:",
"pocket_read_even_more": "View More Stories",
"pocket_more_reccommendations": "More Recommendations",
"pocket_learn_more": "Learn More",
"pocket_cta_button": "Get Pocket",
"pocket_cta_text": "Save the stories you love in Pocket, and fuel your mind with fascinating reads.",
"highlights_empty_state": "Start browsing, and well show some of the great articles, videos, and other pages youve recently visited or bookmarked here.",
"topstories_empty_state": "Youve caught up. Check back later for more top stories from {provider}. Cant wait? Select a popular topic to find more great stories from around the web.",
"manual_migration_explanation2": "Try Firefox with the bookmarks, history and passwords from another browser.",
"manual_migration_cancel_button": "No Thanks",
"manual_migration_import_button": "Import Now",
"error_fallback_default_info": "Oops, something went wrong loading this content.",
"error_fallback_default_refresh_suggestion": "Refresh page to try again.",
"section_menu_action_remove_section": "Remove Section",
"section_menu_action_collapse_section": "Collapse Section",
"section_menu_action_expand_section": "Expand Section",
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_manage_webext": "Manage Extension",
"section_menu_action_add_topsite": "Add Top Site",
"section_menu_action_add_search_engine": "Add Search Engine",
"section_menu_action_move_up": "Move Up",
"section_menu_action_move_down": "Move Down",
"section_menu_action_privacy_notice": "Privacy Notice",
"firstrun_title": "Take Firefox with You",
"firstrun_content": "Get your bookmarks, history, passwords and other settings on all your devices.",
"firstrun_learn_more_link": "Learn more about Firefox Accounts",
"firstrun_form_header": "Enter your email",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "Email",
"firstrun_invalid_input": "Valid email required",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "Terms of Service",
"firstrun_privacy_notice": "Privacy Notice",
"firstrun_continue_to_login": "Continue",
"firstrun_skip_login": "Skip this step"
};

View File

@ -0,0 +1,39 @@
<!doctype html>
<html lang="is" dir="ltr">
<head>
<meta charset="utf-8">
<meta http-equiv="Content-Security-Policy" content="default-src 'none'; script-src 'unsafe-inline' resource: chrome:; connect-src https:; img-src https: data: blob:; style-src 'unsafe-inline';">
<title>Nýr flipi</title>
<link rel="icon" type="image/png" href="chrome://branding/content/icon32.png"/>
<link rel="stylesheet" href="chrome://browser/content/contentSearchUI.css" />
<link rel="stylesheet" href="resource://activity-stream/css/activity-stream.css" />
</head>
<body class="activity-stream">
<div id="root"></div>
<div id="snippets-container">
<div id="snippets"></div>
</div>
<script>
// Don't directly load the following scripts as part of html to let the page
// finish loading to render the content sooner.
for (const src of [
"chrome://browser/content/contentSearchUI.js",
"chrome://browser/content/contentTheme.js",
"resource://activity-stream/vendor/react.js",
"resource://activity-stream/vendor/react-dom.js",
"resource://activity-stream/vendor/prop-types.js",
"resource://activity-stream/vendor/react-intl.js",
"resource://activity-stream/vendor/redux.js",
"resource://activity-stream/vendor/react-redux.js",
"resource://activity-stream/prerendered/is/activity-stream-strings.js",
"resource://activity-stream/data/content/activity-stream.bundle.js"
]) {
// These dynamically inserted scripts by default are async, but we need them
// to load in the desired order (i.e., bundle last).
const script = document.body.appendChild(document.createElement("script"));
script.async = false;
script.src = src;
}
</script>
</body>
</html>

View File

@ -76,7 +76,7 @@ window.gActivityStreamStrings = {
"pocket_read_even_more": "Vis fleire saker",
"pocket_more_reccommendations": "More Recommendations",
"pocket_learn_more": "Les meir",
"pocket_cta_button": "Get Pocket",
"pocket_cta_button": "Last ned Pocket",
"pocket_cta_text": "Save the stories you love in Pocket, and fuel your mind with fascinating reads.",
"highlights_empty_state": "Begynn å surfe, og vi vil vise deg nokre av dei beste artiklane, videoane og andre sider du nyleg har besøkt eller bokmerka her.",
"topstories_empty_state": "Det finst ikkje fleire. Kom tilbake seinare for fleire topphistoriar frå {provider}. Kan du ikkje vente? Vel eit populært emne for å finne fleire gode artiklar frå heile nettet.",

View File

@ -75,7 +75,7 @@ window.gActivityStreamStrings = {
"pocket_read_more": "Tèmas populars:",
"pocket_read_even_more": "Veire mai darticles",
"pocket_more_reccommendations": "More Recommendations",
"pocket_learn_more": "Learn More",
"pocket_learn_more": "Ne saber mai",
"pocket_cta_button": "Get Pocket",
"pocket_cta_text": "Save the stories you love in Pocket, and fuel your mind with fascinating reads.",
"highlights_empty_state": "Començatz de navegar e aquí vos mostrarem los melhors articles, vidèos e autras paginas quavètz visitadas o apondudas als marcapaginas.",

View File

@ -74,10 +74,10 @@ window.gActivityStreamStrings = {
"topsites_form_image_validation": "Wczytanie obrazu nie powiodło się. Spróbuj innego adresu.",
"pocket_read_more": "Popularne treści:",
"pocket_read_even_more": "Pokaż więcej artykułów",
"pocket_more_reccommendations": "More Recommendations",
"pocket_learn_more": "Learn More",
"pocket_cta_button": "Get Pocket",
"pocket_cta_text": "Save the stories you love in Pocket, and fuel your mind with fascinating reads.",
"pocket_more_reccommendations": "Więcej polecanych",
"pocket_learn_more": "Więcej informacji",
"pocket_cta_button": "Pobierz Pocket",
"pocket_cta_text": "Zachowuj historie w Pocket, aby wrócić później do ich lektury.",
"highlights_empty_state": "Zacznij przeglądać Internet, a pojawią się tutaj świetne artykuły, filmy oraz inne ostatnio odwiedzane strony i dodane zakładki.",
"topstories_empty_state": "To na razie wszystko. {provider} później będzie mieć więcej popularnych artykułów. Nie możesz się doczekać? Wybierz popularny temat, aby znaleźć więcej treści z całego Internetu.",
"manual_migration_explanation2": "Wypróbuj program Firefox z zakładkami, historią i hasłami z innej przeglądarki.",

View File

@ -75,9 +75,9 @@ window.gActivityStreamStrings = {
"pocket_read_more": "Tópicos populares:",
"pocket_read_even_more": "Ver mais histórias",
"pocket_more_reccommendations": "Mais recomendações",
"pocket_learn_more": "Saber mais",
"pocket_learn_more": "Saiba mais",
"pocket_cta_button": "Obter o Pocket",
"pocket_cta_text": "Guarde as histórias que adora no Pocket, e abasteça a sua mente com leituras fascinantes.",
"pocket_cta_text": "Salve as histórias que você gosta no Pocket e abasteça sua mente com leituras fascinantes.",
"highlights_empty_state": "Comece a navegar e nós mostraremos aqui alguns ótimos artigos, vídeos e outras páginas que você favoritou ou visitou recentemente.",
"topstories_empty_state": "Você já viu tudo. Volte mais tarde para mais histórias do {provider}. Não consegue esperar? Escolha um assunto popular para encontrar mais grandes histórias através da web.",
"manual_migration_explanation2": "Experimente o Firefox com os favoritos, histórico e senhas salvas em outro navegador.",

View File

@ -77,7 +77,7 @@ window.gActivityStreamStrings = {
"pocket_more_reccommendations": "Ďalšie odporúčania",
"pocket_learn_more": "Ďalšie informácie",
"pocket_cta_button": "Získajte Pocket",
"pocket_cta_text": "Save the stories you love in Pocket, and fuel your mind with fascinating reads.",
"pocket_cta_text": "Ukladajte si články do služby Pocket a užívajte si skvelé čítanie.",
"highlights_empty_state": "Začnite s prehliadaním a my vám na tomto mieste ukážeme skvelé články, videá a ostatné stránky, ktoré ste nedávno navštívili alebo pridali medzi záložky.",
"topstories_empty_state": "Už ste prečítali všetko. Ďalšie príbehy zo služby {provider} tu nájdete opäť neskôr. Nemôžete sa dočkať? Vyberte si populárnu tému a pozrite sa na ďalšie skvelé príbehy z celého webu.",
"manual_migration_explanation2": "Vyskúšajte Firefox so záložkami, históriou prehliadania a heslami s iných prehliadačov.",

View File

@ -24,13 +24,13 @@ window.gActivityStreamStrings = {
"menu_action_save_to_pocket": "Pocket میں محفوظ کریں",
"menu_action_delete_pocket": "Pocket سے جزف کریں",
"menu_action_archive_pocket": "Archive in Pocket",
"menu_action_show_file_mac_os": "Show in Finder",
"menu_action_show_file_mac_os": "تلاش کار میں دکھائیں",
"menu_action_show_file_windows": "حامل پوشہ کھولیں",
"menu_action_show_file_linux": "Open Containing Folder",
"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": "Go to Download Page",
"menu_action_go_to_download_page": "ڈاؤن لوڈ صفحہ پر جائیں",
"menu_action_remove_download": "سابقات سے ہٹائیں",
"search_button": "تلاش",
"search_header": "{search_engine_name} پر تلاش کریں",
@ -101,7 +101,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "اپنی ای میل داخل کریں",
"firstrun_form_sub_header": "to continue to Firefox Sync",
"firstrun_email_input_placeholder": "ای میل",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "جائز ای میل کی ظرورت ہے",
"firstrun_extra_legal_links": "By proceeding, you agree to the {terms} and {privacy}.",
"firstrun_terms_of_service": "خدمت کی شرائط",
"firstrun_privacy_notice": "رازداری کا نوٹس",

View File

@ -1,4 +1,4 @@
const {ASRouterTargeting, TopFrecentSitesCache, TotalBookmarksCountCache} =
const {ASRouterTargeting, QueryCache} =
ChromeUtils.import("resource://activity-stream/lib/ASRouterTargeting.jsm", {});
const {AddonTestUtils} =
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
@ -90,6 +90,24 @@ add_task(async function check_other_error_handling() {
});
// ASRouterTargeting.Environment
add_task(async function check_locale() {
ok(Services.locale.appLocaleAsLangTag, "Services.locale.appLocaleAsLangTag exists");
const message = {id: "foo", targeting: `locale == "${Services.locale.appLocaleAsLangTag}"`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item when filtering by locale");
});
add_task(async function check_localeLanguageCode() {
const currentLanguageCode = Services.locale.appLocaleAsLangTag.substr(0, 2);
is(
Services.locale.negotiateLanguages([currentLanguageCode], [Services.locale.appLocaleAsLangTag])[0],
Services.locale.appLocaleAsLangTag,
"currentLanguageCode should resolve to the current locale (e.g en => en-US)"
);
const message = {id: "foo", targeting: `localeLanguageCode == "${currentLanguageCode}"`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"should select correct item when filtering by localeLanguageCode");
});
add_task(async function checkProfileAgeCreated() {
let profileAccessor = new ProfileAge();
is(await ASRouterTargeting.Environment.profileAgeCreated, await profileAccessor.created,
@ -135,8 +153,9 @@ add_task(async function check_totalBookmarksCount() {
await clearHistoryAndBookmarks();
const message = {id: "foo", targeting: "totalBookmarksCount > 0"};
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), undefined,
"Should not select any message because");
const results = await ASRouterTargeting.findMatchingMessage({messages: [message]});
is(results ? JSON.stringify(results) : results, undefined,
"Should not select any message because bookmarks count is not 0");
const bookmark = await PlacesUtils.bookmarks.insert({
parentGuid: PlacesUtils.bookmarks.unfiledGuid,
@ -144,7 +163,7 @@ add_task(async function check_totalBookmarksCount() {
url: "https://mozilla1.com/nowNew",
});
TotalBookmarksCountCache.expire();
QueryCache.queries.TotalBookmarksCount.expire();
is(await ASRouterTargeting.findMatchingMessage({messages: [message]}), message,
"Should select correct item after bookmarks are added.");
@ -305,7 +324,6 @@ add_task(async function checkFrecentSites() {
// Cleanup
await clearHistoryAndBookmarks();
TopFrecentSitesCache.expire();
});
add_task(async function check_firefox_version() {

View File

@ -2,6 +2,8 @@
ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
"resource://testing-common/PlacesTestUtils.jsm");
ChromeUtils.defineModuleGetter(this, "QueryCache",
"resource://activity-stream/lib/ASRouterTargeting.jsm");
function popPrefs() {
return SpecialPowers.popPrefEnv();
@ -22,6 +24,7 @@ async function setDefaultTopSites() { // eslint-disable-line no-unused-vars
async function clearHistoryAndBookmarks() { // eslint-disable-line no-unused-vars
await PlacesUtils.bookmarks.eraseEverything();
await PlacesUtils.history.clear();
QueryCache.expireAll();
}
/**

View File

@ -99,6 +99,7 @@ export const UserEventAction = Joi.object().keys({
icon_type: Joi.valid(["tippytop", "rich_icon", "screenshot_with_icon", "screenshot", "no_image"]),
card_type: Joi.valid(["bookmark", "trending", "pinned", "pocket", "search"]),
search_vendor: Joi.valid(["google", "amazon"]),
has_flow_params: Joi.bool(),
}),
}).required(),
meta: Joi.object().keys({

View File

@ -16,6 +16,7 @@ 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";
import {QueryCache} from "lib/ASRouterTargeting.jsm";
const MESSAGE_PROVIDER_PREF_NAME = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const ONBOARDING_FINISHED_PREF = "browser.onboarding.notification.finished";
@ -201,6 +202,18 @@ describe("ASRouter", () => {
assert.lengthOf(Router.state.providers, length);
assert.isDefined(provider);
});
it("should update the list of providers and load messages on legacy onboarding pref change", async () => {
sandbox.spy(Router, "_updateMessageProviders");
sandbox.spy(Router, "loadMessagesFromAllProviders");
// we do NOT want to set the onboarding pref again and cause an infinite loop
sandbox.stub(global.Services.prefs, "setBoolPref");
await Router.observe(null, null, ONBOARDING_FINISHED_PREF);
assert.calledOnce(Router._updateMessageProviders);
assert.calledOnce(Router.loadMessagesFromAllProviders);
assert.notCalled(global.Services.prefs.setBoolPref);
});
});
describe("setState", () => {
@ -219,7 +232,7 @@ describe("ASRouter", () => {
});
describe("#overrideOrEnableLegacyOnboarding", () => {
it("should set the onboarding finished pref to true if it is true and ASRPreferences.allowLegacyOnboarding is false", async () => {
it("should set the onboarding finished pref to true if legacy onboarding is NOT allowed", async () => {
sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(ONBOARDING_FINISHED_PREF).returns(false);
sandbox.stub(ASRouterPreferences, "specialConditions").get(() => ({allowLegacyOnboarding: false}));
const setStub = sandbox.stub(global.Services.prefs, "setBoolPref");
@ -227,9 +240,15 @@ describe("ASRouter", () => {
Router.overrideOrEnableLegacyOnboarding();
assert.calledWith(setStub, ONBOARDING_FINISHED_PREF, true);
});
it("should set the onboarding finished pref to false if it is false and ASRPreferences.allowLegacyOnboarding is true", async () => {
sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(ONBOARDING_FINISHED_PREF).returns(true);
sandbox.stub(ASRouterPreferences, "specialConditions").get(() => ({allowLegacyOnboarding: true}));
it("should set the onboarding finished pref to false if ASRPreferences.allowLegacyOnboarding is true and we previously override onboarding", async () => {
// First override the finished pref to be to true so we can reverse the overriding
const onboardingPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(ONBOARDING_FINISHED_PREF).returns(false);
const specialConditionsStub = sandbox.stub(ASRouterPreferences, "specialConditions").get(() => ({allowLegacyOnboarding: false}));
Router.overrideOrEnableLegacyOnboarding();
// Now reverse it
onboardingPrefStub.withArgs(ONBOARDING_FINISHED_PREF).returns(true);
specialConditionsStub.get(() => ({allowLegacyOnboarding: true}));
const setStub = sandbox.stub(global.Services.prefs, "setBoolPref");
Router.overrideOrEnableLegacyOnboarding();
@ -365,6 +384,15 @@ describe("ASRouter", () => {
assert.equal(Router.state.providers.length, 1);
assert.equal(Router.state.providers[0].id, providers[1].id);
});
it("should not add snippets if legacy onboarding is not finished", () => {
sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(ONBOARDING_FINISHED_PREF).returns(false);
const providers = [
{id: "snippets", enabled: true, type: "remote", url: "https://www.foo.com/"},
];
setMessageProviderPref(providers);
Router._updateMessageProviders();
assert.equal(Router.state.providers.length, 0);
});
});
describe("blocking", () => {
@ -432,16 +460,16 @@ describe("ASRouter", () => {
});
});
describe("#onMessage: CONNECT_UI_REQUEST", () => {
describe("#onMessage: SNIPPETS_REQUEST", () => {
it("should set state.lastMessageId to a message id", async () => {
await Router.onMessage(fakeAsyncMessage({type: "CONNECT_UI_REQUEST"}));
await Router.onMessage(fakeAsyncMessage({type: "SNIPPETS_REQUEST"}));
assert.include(ALL_MESSAGE_IDS, Router.state.lastMessageId);
});
it("should send a message back to the to the target", async () => {
// force the only message to be a regular message so getRandomItemFromArray picks it
await Router.setState({messages: [{id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}}]});
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_MESSAGE", data: currentMessage});
@ -450,7 +478,7 @@ describe("ASRouter", () => {
// force the only message to be a bundled message so getRandomItemFromArray picks it
sandbox.stub(Router, "_findProvider").returns(null);
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 1, content: {title: "Foo1", body: "Foo123-1"}}]});
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
const [currentMessage] = Router.state.messages.filter(message => message.id === Router.state.lastMessageId);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
@ -463,7 +491,7 @@ describe("ASRouter", () => {
const firstMessage = {id: "foo2", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo2", body: "Foo123-2"}};
const secondMessage = {id: "foo1", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo1", body: "Foo123-1"}};
await Router.setState({messages: [secondMessage, firstMessage]});
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
@ -485,20 +513,20 @@ describe("ASRouter", () => {
it("should send a CLEAR_ALL message if no bundle available", async () => {
// force the only message to be a bundled message that needs 2 messages in the bundle
await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should send a CLEAR_ALL message if no messages are available", async () => {
await Router.setState({messages: []});
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "CLEAR_ALL"});
});
it("should make a request to the provided endpoint on CONNECT_UI_REQUEST", async () => {
it("should make a request to the provided endpoint on SNIPPETS_REQUEST", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST", data: {endpoint: {url}}});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWith(global.fetch, url);
@ -514,21 +542,21 @@ describe("ASRouter", () => {
});
it("should dispatch SNIPPETS_PREVIEW_MODE when adding a preview endpoint", async () => {
const url = "https://snippets-admin.mozilla.org/foo";
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST", data: {endpoint: {url}}});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.calledWithExactly(Router.dispatchToAS, ac.OnlyToOneContent({type: "SNIPPETS_PREVIEW_MODE"}, msg.target.portID));
});
it("should not add a url that is not from a whitelisted host", async () => {
const url = "https://mozilla.org";
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST", data: {endpoint: {url}}});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
});
it("should reject bad urls", async () => {
const url = "foo";
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST", data: {endpoint: {url}}});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST", data: {endpoint: {url}}});
await Router.onMessage(msg);
assert.lengthOf(Router.state.providers.filter(p => p.url === url), 0);
@ -621,15 +649,14 @@ describe("ASRouter", () => {
it("should send a message containing the whole state", async () => {
const msg = fakeAsyncMessage({type: "ADMIN_CONNECT_STATE"});
await Router.onMessage(msg);
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "ADMIN_SET_STATE", data: Router.state});
});
});
describe("#onMessage: CONNECT_UI_REQUEST", () => {
it("should call sendNextMessage on CONNECT_UI_REQUEST", async () => {
describe("#onMessage: SNIPPETS_REQUEST", () => {
it("should call sendNextMessage on SNIPPETS_REQUEST", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
await Router.onMessage(msg);
@ -686,7 +713,7 @@ describe("ASRouter", () => {
});
it("should get the bundle and send the message if the message has a bundle", async () => {
sandbox.stub(Router, "sendNextMessage").resolves();
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
msg.bundled = 2; // force this message to want to be bundled
await Router.onMessage(msg);
assert.calledOnce(Router.sendNextMessage);
@ -837,6 +864,17 @@ describe("ASRouter", () => {
});
});
describe("#onMessage: EXPIRE_QUERY_CACHE", () => {
it("should clear all QueryCache getters", async () => {
const msg = fakeAsyncMessage({type: "EXPIRE_QUERY_CACHE"});
sandbox.stub(QueryCache, "expireAll");
await Router.onMessage(msg);
assert.calledOnce(QueryCache.expireAll);
});
});
describe("_triggerHandler", () => {
it("should call #onMessage with the correct trigger", () => {
sinon.spy(Router, "onMessage");
@ -1139,7 +1177,7 @@ describe("ASRouter", () => {
it("should dispatch an event when a targeting expression throws an error", async () => {
sandbox.stub(global.FilterExpressions, "eval").returns(Promise.reject(new Error("fake error")));
await Router.setState({messages: [{id: "foo", targeting: "foo2.[[("}]});
const msg = fakeAsyncMessage({type: "CONNECT_UI_REQUEST"});
const msg = fakeAsyncMessage({type: "SNIPPETS_REQUEST"});
dispatchStub.reset();
await Router.onMessage(msg);

View File

@ -3,6 +3,14 @@ const FAKE_PROVIDERS = [{id: "foo"}, {id: "bar"}];
const PROVIDER_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const DEVTOOLS_PREF = "browser.newtabpage.activity-stream.asrouter.devtoolsEnabled";
const SNIPPETS_USER_PREF = "browser.newtabpage.activity-stream.feeds.snippets";
/** NUMBER_OF_PREFS_TO_OBSERVE includes:
* 1. asrouter.messageProvider
* 2. asrouter.devtoolsEnabled
* 3. browser.newtabpage.activity-stream.feeds.snippets (user preference - snippets)
*/
const NUMBER_OF_PREFS_TO_OBSERVE = 3;
describe("ASRouterPreferences", () => {
let ASRouterPreferences;
@ -16,7 +24,7 @@ describe("ASRouterPreferences", () => {
sandbox = sinon.sandbox.create();
addObserverStub = sandbox.stub(global.Services.prefs, "addObserver");
stringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref").withArgs(PROVIDER_PREF).returns(JSON.stringify(FAKE_PROVIDERS));
boolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref").withArgs(DEVTOOLS_PREF).returns(false);
boolPrefStub = sandbox.stub(global.Services.prefs, "getBoolPref").returns(false);
});
afterEach(() => {
sandbox.restore();
@ -29,12 +37,12 @@ describe("ASRouterPreferences", () => {
ASRouterPreferences.init();
assert.isTrue(ASRouterPreferences._initialized);
});
it("should set two observers and not re-initialize if already initialized", () => {
it(`should set ${NUMBER_OF_PREFS_TO_OBSERVE} observers and not re-initialize if already initialized`, () => {
ASRouterPreferences.init();
assert.calledTwice(addObserverStub);
assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);
ASRouterPreferences.init();
ASRouterPreferences.init();
assert.calledTwice(addObserverStub);
assert.callCount(addObserverStub, NUMBER_OF_PREFS_TO_OBSERVE);
});
});
describe("#uninit", () => {
@ -64,7 +72,7 @@ describe("ASRouterPreferences", () => {
// Tests to make sure we don't remove observers that weren't set
ASRouterPreferences.uninit();
assert.calledTwice(removeStub);
assert.callCount(removeStub, NUMBER_OF_PREFS_TO_OBSERVE);
assert.calledWith(removeStub, PROVIDER_PREF);
assert.calledWith(removeStub, DEVTOOLS_PREF);
assert.isEmpty(ASRouterPreferences._callbacks);
@ -163,6 +171,12 @@ describe("ASRouterPreferences", () => {
assert.isFalse(ASRouterPreferences.specialConditions.allowLegacySnippets);
});
});
describe("#getUserPreference(providerId)", () => {
it("should return the user preference for snippets", () => {
boolPrefStub.withArgs(SNIPPETS_USER_PREF).returns(true);
assert.isTrue(ASRouterPreferences.getUserPreference("snippets"));
});
});
describe("observer, listeners", () => {
it("should invalidate .providers when the pref is changed", () => {
const testProviders = [{id: "newstuff"}];

View File

@ -65,14 +65,14 @@ describe("#CachedTargetingGetter", () => {
frecentStub.resolves();
clock.tick(sixHours);
await topsitesCache.getTopFrecentSites; // eslint-disable-line no-unused-expressions
await topsitesCache.getTopFrecentSites; // eslint-disable-line no-unused-expressions
await topsitesCache.get();
await topsitesCache.get();
assert.calledOnce(global.NewTabUtils.activityStreamProvider.getTopFrecentSites);
clock.tick(sixHours);
await topsitesCache.getTopFrecentSites; // eslint-disable-line no-unused-expressions
await topsitesCache.get();
assert.calledTwice(global.NewTabUtils.activityStreamProvider.getTopFrecentSites);
});
@ -83,7 +83,7 @@ describe("#CachedTargetingGetter", () => {
// assert.throws expect a function as the first parameter, try/catch is a
// workaround
try {
await topsitesCache.getTopFrecentSites; // eslint-disable-line no-unused-expressions
await topsitesCache.get();
assert.isTrue(false);
} catch (e) {
assert.calledOnce(global.Cu.reportError);

View File

@ -23,7 +23,7 @@ describe("<StartupOverlay>", () => {
assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith(dispatch, ac.UserEvent({event: at.SKIPPED_SIGNIN}));
assert.calledWith(dispatch, ac.UserEvent({event: at.SKIPPED_SIGNIN, value: {has_flow_params: false}}));
});
it("should emit UserEvent SUBMIT_EMAIL when you submit the form", () => {
@ -33,6 +33,6 @@ describe("<StartupOverlay>", () => {
assert.calledOnce(dispatch);
assert.isUserEventAction(dispatch.firstCall.args[0]);
assert.calledWith(dispatch, ac.UserEvent({event: at.SUBMIT_EMAIL}));
assert.calledWith(dispatch, ac.UserEvent({event: at.SUBMIT_EMAIL, value: {has_flow_params: false}}));
});
});

View File

@ -10,11 +10,12 @@ import {
UserEventPing,
} from "test/schemas/pings";
import {FakePrefs, GlobalOverrider} from "test/unit/utils";
import {ASRouterPreferences} from "lib/ASRouterPreferences.jsm";
import injector from "inject!lib/TelemetryFeed.jsm";
const FAKE_UUID = "{foo-123-foo}";
const FAKE_ROUTER_MESSAGE_PROVIDER = JSON.stringify([{id: "cfr", enabled: true}]);
const FAKE_ROUTER_MESSAGE_PROVIDER_COHORT = JSON.stringify([{id: "cfr", enabled: true, cohort: "cohort_group"}]);
const FAKE_ROUTER_MESSAGE_PROVIDER = [{id: "cfr", enabled: true}];
const FAKE_ROUTER_MESSAGE_PROVIDER_COHORT = [{id: "cfr", enabled: true, cohort: "cohort_group"}];
describe("TelemetryFeed", () => {
let globals;
@ -38,7 +39,6 @@ describe("TelemetryFeed", () => {
PREF_IMPRESSION_ID,
TELEMETRY_PREF,
EVENTS_TELEMETRY_PREF,
ROUTER_MESSAGE_PROVIDER_PREF,
} = injector({
"common/PerfService.jsm": {perfService},
"lib/UTEventReporting.jsm": {UTEventReporting},
@ -52,13 +52,14 @@ describe("TelemetryFeed", () => {
globals.set("gUUIDGenerator", {generateUUID: () => FAKE_UUID});
globals.set("PingCentre", PingCentre);
globals.set("UTEventReporting", UTEventReporting);
FakePrefs.prototype.prefs[ROUTER_MESSAGE_PROVIDER_PREF] = FAKE_ROUTER_MESSAGE_PROVIDER;
sandbox.stub(ASRouterPreferences, "providers").get(() => FAKE_ROUTER_MESSAGE_PROVIDER);
instance = new TelemetryFeed();
});
afterEach(() => {
clock.restore();
globals.restore();
FakePrefs.prototype.prefs = {};
ASRouterPreferences.uninit();
});
describe("#init", () => {
it("should add .pingCentre, a PingCentre instance", () => {
@ -117,20 +118,6 @@ describe("TelemetryFeed", () => {
assert.propertyVal(instance, "eventTelemetryEnabled", true);
});
});
describe("a-s router message provider cohort changes from false to true", () => {
beforeEach(() => {
FakePrefs.prototype.prefs = {};
instance = new TelemetryFeed();
assert.ok(!instance.isInCFRCohort);
});
it("should set the _isInCFRCohort property to true", () => {
instance._prefs.set(ROUTER_MESSAGE_PROVIDER_PREF, FAKE_ROUTER_MESSAGE_PROVIDER_COHORT);
assert.ok(instance.isInCFRCohort);
});
});
});
describe("#addSession", () => {
it("should add a session and return it", () => {
@ -488,18 +475,6 @@ describe("TelemetryFeed", () => {
assert.propertyVal(ping, "tiles", tiles);
});
});
describe("#_parseCFRCohort", () => {
it("should return true if it is in the CFR cohort", () => {
assert.ok(instance._parseCFRCohort(FAKE_ROUTER_MESSAGE_PROVIDER_COHORT));
});
it("should return false if it is not in the CFR cohort", () => {
assert.ok(!instance._parseCFRCohort(FAKE_ROUTER_MESSAGE_PROVIDER));
});
it("should report an error given an invalid pref", () => {
assert.ok(!instance._parseCFRCohort("some in valid json string"));
assert.called(global.Cu.reportError);
});
});
describe("#applyCFRPolicy", () => {
it("should use client_id and message_id in prerelease", () => {
globals.set("UpdateUtils", {getUpdateChannel() { return "nightly"; }});
@ -539,7 +514,7 @@ describe("TelemetryFeed", () => {
});
it("should use client_id and message_id in the experiment cohort in release", () => {
globals.set("UpdateUtils", {getUpdateChannel() { return "release"; }});
FakePrefs.prototype.prefs[ROUTER_MESSAGE_PROVIDER_PREF] = FAKE_ROUTER_MESSAGE_PROVIDER_COHORT;
sandbox.stub(ASRouterPreferences, "providers").get(() => FAKE_ROUTER_MESSAGE_PROVIDER_COHORT);
const data = {
action: "cfr_user_event",
source: "CFR",
@ -750,15 +725,6 @@ describe("TelemetryFeed", () => {
assert.notProperty(instance._prefs.observers, EVENTS_TELEMETRY_PREF);
});
it("should remove the a-s router message provider listener", () => {
instance = new TelemetryFeed();
assert.property(instance._prefs.observers, ROUTER_MESSAGE_PROVIDER_PREF);
instance.uninit();
assert.notProperty(instance._prefs.observers, ROUTER_MESSAGE_PROVIDER_PREF);
});
it("should call Cu.reportError if this._prefs.ignore throws", () => {
globals.sandbox.stub(FakePrefs.prototype, "ignore").throws("Some Error");
instance = new TelemetryFeed();