Bug 1478870 - Add component-ified tooling, ordered onboarding and bug fixes to Activity Stream. r=ursula

MozReview-Commit-ID: K14RSdAbVH7

--HG--
extra : rebase_source : f84c082385d46e401bae2ff17a71bb5f54a932cf
This commit is contained in:
Ed Lee 2018-07-27 13:01:36 -07:00
Родитель 6cf31b8253
Коммит 8716065e9c
38 изменённых файлов: 247 добавлений и 204 удалений

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

@ -20,7 +20,7 @@ const TOPIC_CONTENT_DOCUMENT_INTERACTIVE = "content-document-interactive";
// Automated tests ensure packaged locales are in this list. Copied output of: // 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 // https://github.com/mozilla/activity-stream/blob/master/bin/render-activity-stream-html.js
const ACTIVITY_STREAM_LOCALES = "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-mac 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 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 ABOUT_URL = "about:newtab";
const BASE_URL = "resource://activity-stream/"; const BASE_URL = "resource://activity-stream/";
@ -35,7 +35,6 @@ const PREF_SEPARATE_PRIVILEGED_CONTENT_PROCESS = "browser.tabs.remote.separatePr
const PREF_ACTIVITY_STREAM_PRERENDER_ENABLED = "browser.newtabpage.activity-stream.prerender"; const PREF_ACTIVITY_STREAM_PRERENDER_ENABLED = "browser.newtabpage.activity-stream.prerender";
const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug"; const PREF_ACTIVITY_STREAM_DEBUG = "browser.newtabpage.activity-stream.debug";
function AboutNewTabService() { function AboutNewTabService() {
Services.obs.addObserver(this, TOPIC_APP_QUIT); Services.obs.addObserver(this, TOPIC_APP_QUIT);
Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE); Services.obs.addObserver(this, TOPIC_LOCALES_CHANGE);
@ -103,9 +102,7 @@ AboutNewTabService.prototype = {
Ci.nsIAboutNewTabService, Ci.nsIAboutNewTabService,
Ci.nsIObserver Ci.nsIObserver
]), ]),
_xpcom_categories: [{ _xpcom_categories: [{service: true}],
service: true
}],
observe(subject, topic, data) { observe(subject, topic, data) {
switch (topic) { switch (topic) {
@ -121,7 +118,7 @@ AboutNewTabService.prototype = {
this.notifyChange(); this.notifyChange();
} }
break; break;
case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: case TOPIC_CONTENT_DOCUMENT_INTERACTIVE: {
const win = subject.defaultView; const win = subject.defaultView;
// It seems like "content-document-interactive" is triggered multiple // It seems like "content-document-interactive" is triggered multiple
@ -178,6 +175,7 @@ AboutNewTabService.prototype = {
}; };
subject.addEventListener("unload", onUnloaded, {once: true}); subject.addEventListener("unload", onUnloaded, {once: true});
break; break;
}
case TOPIC_APP_QUIT: case TOPIC_APP_QUIT:
this.uninit(); this.uninit();
if (IS_MAIN_PROCESS) { if (IS_MAIN_PROCESS) {
@ -277,17 +275,17 @@ AboutNewTabService.prototype = {
}, },
set newTabURL(aNewTabURL) { set newTabURL(aNewTabURL) {
aNewTabURL = aNewTabURL.trim(); let newTabURL = aNewTabURL.trim();
if (aNewTabURL === ABOUT_URL) { if (newTabURL === ABOUT_URL) {
// avoid infinite redirects in case one sets the URL to about:newtab // avoid infinite redirects in case one sets the URL to about:newtab
this.resetNewTabURL(); this.resetNewTabURL();
return; return;
} else if (aNewTabURL === "") { } else if (newTabURL === "") {
aNewTabURL = "about:blank"; newTabURL = "about:blank";
} }
this.toggleActivityStream(false); this.toggleActivityStream(false);
this._newTabURL = aNewTabURL; this._newTabURL = newTabURL;
this._overridden = true; this._overridden = true;
this.notifyChange(); this.notifyChange();
}, },
@ -311,11 +309,14 @@ AboutNewTabService.prototype = {
get activityStreamLocale() { get activityStreamLocale() {
// Pick the best available locale to match the app locales // Pick the best available locale to match the app locales
return Services.locale.negotiateLanguages( return Services.locale.negotiateLanguages(
Services.locale.getAppLocalesAsLangTags(), Services.locale.getAppLocalesAsBCP47(),
ACTIVITY_STREAM_LOCALES, ACTIVITY_STREAM_BCP47,
// defaultLocale's strings aren't necessarily packaged, but en-US' are // defaultLocale's strings aren't necessarily packaged, but en-US' are
"en-US" "en-US",
)[0]; Services.locale.langNegStrategyLookup
// Convert the BCP47 to lang tag, which is what is used in our paths, as a
// workaround for bug 1478930 negotiating incorrectly with lang tags
)[0].replace(/^(ja-JP-mac)os$/, "$1");
}, },
resetNewTabURL() { resetNewTabURL() {

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

@ -26,7 +26,7 @@ else
FIREFOX_PATH=${AS_PINE_TEST_DIR}/mozilla-central FIREFOX_PATH=${AS_PINE_TEST_DIR}/mozilla-central
fi fi
MC_MODULE_PATH="$FIREFOX_PATH/browser/extensions/activity-stream" MC_MODULE_PATH="$FIREFOX_PATH/browser/components/newtab"
# By default, just use mozilla-central + the export. If ENABLE_MC_AS is set to # By default, just use mozilla-central + the export. If ENABLE_MC_AS is set to
# 1, patch on top of mozilla-central + the export to turn on the AS pref and # 1, patch on top of mozilla-central + the export to turn on the AS pref and

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

@ -1,21 +0,0 @@
#! /usr/bin/env node
"use strict";
const MIN_FIREFOX_VERSION = "55.0a1";
/* globals cd, mv, sed */
require("shelljs/global");
cd(process.argv[2]);
// Convert install.rdf.in to install.rdf without substitutions
mv("install.rdf.in", "install.rdf");
sed("-i", /^#filter substitution/, "", "install.rdf");
sed("-i", /(<em:minVersion>).+(<\/em:minVersion>)/, `$1${MIN_FIREFOX_VERSION}$2`, "install.rdf");
sed("-i", /(<em:maxVersion>).+(<\/em:maxVersion>)/, "$1*$2", "install.rdf");
// Convert jar.mn to chrome.manifest with just manifest
mv("jar.mn", "chrome.manifest");
sed("-i", /^[^%].*$/, "", "chrome.manifest");
sed("-i", /^% (content.*) %(.*)$/, "$1 $2", "chrome.manifest");
sed("-i", /^% (resource.*) %.*$/, "$1 .", "chrome.manifest");

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

@ -245,8 +245,11 @@ function main() { // eslint-disable-line max-statements
console.log("\x1b[33m", `Skipped the following locales because they are not in CENTRAL_LOCALES: ${extraLocales.join(", ")}`, "\x1b[0m"); console.log("\x1b[33m", `Skipped the following locales because they are not in CENTRAL_LOCALES: ${extraLocales.join(", ")}`, "\x1b[0m");
} }
// Convert ja-JP-mac lang tag to ja-JP-macos bcp47 to work around bug 1478930
const bcp47String = localizedLocales.join(" ").replace(/(ja-JP-mac)/, "$1os");
// Provide some help to copy/paste locales if tests are failing // Provide some help to copy/paste locales if tests are failing
console.log(`\nIf aboutNewTabService tests are failing for unexpected locales, make sure its list is updated:\nconst ACTIVITY_STREAM_LOCALES = "${localizedLocales.join(" ")}".split(" ");`); console.log(`\nIf aboutNewTabService tests are failing for unexpected locales, make sure its list is updated:\nconst ACTIVITY_STREAM_BCP47 = "${bcp47String}".split(" ");`);
} }
main(); main();

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

@ -1,62 +0,0 @@
#! /usr/bin/env node
/* globals cd, sed */
"use strict";
/**
* Generate update install.rdf.in in the given directory with a version string
* composed of YYYY.MM.DD.${minuteOfDay}-${github_commit_hash}.
*
* @note The github hash is taken from the github repo in the current directory
* the script is run in.
*
* @note The minute of the day was chosen so that the version number is
* (more-or-less) consistently increasing (modulo clock-skew and builds that
* happen within a minute of each other), and although it's UTC, it won't likely
* be confused with something in a readers own time zone.
*
* @example generated version string: 2017.08.28.1217-ebda466c
*/
const process = require("process");
require("shelljs/global");
const simpleGit = require("simple-git")(process.cwd());
const time = new Date();
const minuteOfDay = time.getUTCHours() * 60 + time.getUTCMinutes();
/**
* Return the given string padded with 0s out to the given width.
*
* XXX we should ditch this function in favor of using padStart once
* we start requiring Node 8.
*
* @param {any} s - the string to pad, will be coerced to String first
* @param {Number} width - what's the desired width?
*/
function zeroPadStart(s, width) {
let padded = String(s);
while (padded.length < width) {
padded = `0${padded}`;
}
return padded;
}
// git rev-parse --short HEAD
simpleGit.revparse(["--short", "HEAD"], (err, gitHash) => {
if (err) {
// eslint-disable-next-line no-console
console.error(`SimpleGit.revparse failed: ${err}`);
throw new Error(`SimpleGit.revparse failed: ${err}`);
}
// eslint-disable-next-line prefer-template
let versionString = String(time.getUTCFullYear()) +
"." + zeroPadStart(time.getUTCMonth() + 1, 2) +
"." + zeroPadStart(time.getUTCDate(), 2) +
"." + zeroPadStart(minuteOfDay, 4) +
"-" + gitHash.trim();
cd(process.argv[2]);
sed("-i", /(<em:version>).+(<\/em:version>)$/, `$1${versionString}$2`,
"install.rdf.in");
});

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

@ -7,6 +7,8 @@ Field name | Type | Required | Description | Example / Note
`publish_start` | `date` | No | When to start showing the message | `1524474850876` `publish_start` | `date` | No | When to start showing the message | `1524474850876`
`publish_end` | `date` | No | When to stop 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) `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)
`campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly` `campaign` | `string` | No | Campaign id that the message belongs to | `RustWebAssembly`
`targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes) `targeting` | `string` `JEXL` | No | A [JEXL expression](http://normandy.readthedocs.io/en/latest/user/filter_expressions.html#jexl-basics) with all targeting information needed in order to decide if the message is shown | Not yet implemented, [Examples](#targeting-attributes)
`trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation. `trigger` | `string` | No | An event or condition upon which the message will be immediately shown. This can be combined with `targeting`. Messages that define a trigger will not be shown during non-trigger-based passive message rotation.
@ -31,6 +33,35 @@ Field name | Type | Required | Description | Example / Note
} }
``` ```
### A Bundled Message example
The following 2 messages have a `bundled` property, indicating that they should be shown together, since they have the same template. The number `2` indicates that this message should be shown in a bundle of 2 messages of the same template. The order property defines that ONBOARDING_2 should be shown after ONBOARDING_3 in the bundle.
```javascript
{
id: "ONBOARDING_2",
template: "onboarding",
bundled: 2,
order: 2,
content: {
title: "Private Browsing",
body: "Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web."
},
targeting: "",
trigger: "firstRun"
}
{
id: "ONBOARDING_3",
template: "onboarding",
bundled: 2,
order: 1,
content: {
title: "Find it faster",
body: "Access all of your favorite search engines with a click. Search the whole Web or just one website from the search box."
},
targeting: "",
trigger: "firstRun"
}
```
### HTML subset ### HTML subset
The following tags are allowed in the content of the snippet: `i, b, u, strong, em, br`. The following tags are allowed in the content of the snippet: `i, b, u, strong, em, br`.

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

@ -21,6 +21,15 @@
"description": "An id matching an existing Activity Stream Router template", "description": "An id matching an existing Activity Stream Router template",
"enum": ["simple_snippet"] "enum": ["simple_snippet"]
}, },
"bundled": {
"type": "integer",
"description": "The number of messages of the same template this one should be shown with (optional)"
},
"order": {
"type": "integer",
"minimum": 0,
"description": "If bundled with other messages of the same template, which order should this one be placed in? (optional - defaults to 0)"
},
"content": { "content": {
"type": "object", "type": "object",
"description": "An object containing all variables/props to be rendered in the template. See individual template schemas for details." "description": "An object containing all variables/props to be rendered in the template. See individual template schemas for details."

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

@ -18,7 +18,7 @@ and make sure the `browser.newtabpage.activity-stream.enabled` pref is set to `t
## Source code and submitting pull requests ## Source code and submitting pull requests
A copy of the code in the [system-addon/](../../system-addon/) subdirectory of this repository A copy of the code in the [system-addon/](../../system-addon/) subdirectory of this repository
is exported to Mozilla central on a regular basis, which can be found at [browser/extensions/activity-stream](https://searchfox.org/mozilla-central/source/browser/extensions/activity-stream). is exported to Mozilla central on a regular basis, which can be found at [browser/components/newtab](https://searchfox.org/mozilla-central/source/browser/components/newtab).
Keep in mind that some of these files are generated, so if you intend on editing any files, you should Keep in mind that some of these files are generated, so if you intend on editing any files, you should
do so in the Github version. do so in the Github version.

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

@ -18,10 +18,11 @@ const ONE_HOUR_IN_MS = 60 * 60 * 1000;
const SNIPPETS_ENDPOINT_PREF = "browser.newtabpage.activity-stream.asrouter.snippetsUrl"; const SNIPPETS_ENDPOINT_PREF = "browser.newtabpage.activity-stream.asrouter.snippetsUrl";
// List of hosts for endpoints that serve router messages. // List of hosts for endpoints that serve router messages.
// Key is allowed host, value is a name for the endpoint host. // Key is allowed host, value is a name for the endpoint host.
const WHITELIST_HOSTS = { const DEFAULT_WHITELIST_HOSTS = {
"activity-stream-icons.services.mozilla.com": "production", "activity-stream-icons.services.mozilla.com": "production",
"snippets-admin.mozilla.org": "preview" "snippets-admin.mozilla.org": "preview"
}; };
const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
const MessageLoaderUtils = { const MessageLoaderUtils = {
/** /**
@ -229,6 +230,7 @@ class _ASRouter {
this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage); this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
this._addASRouterPrefListener(); this._addASRouterPrefListener();
this._storage = storage; this._storage = storage;
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
const blockList = await this._storage.get("blockList") || []; const blockList = await this._storage.get("blockList") || [];
const impressions = await this._storage.get("impressions") || {}; const impressions = await this._storage.get("impressions") || {};
@ -283,8 +285,12 @@ class _ASRouter {
return message; return message;
} }
_orderBundle(bundle) {
return bundle.sort((a, b) => a.order - b.order);
}
async _getBundledMessages(originalMessage, target, data, force = false) { async _getBundledMessages(originalMessage, target, data, force = false) {
let result = [{content: originalMessage.content, id: originalMessage.id}]; let result = [{content: originalMessage.content, id: originalMessage.id, order: originalMessage.order || 0}];
// First, find all messages of same template. These are potential matching targeting candidates // First, find all messages of same template. These are potential matching targeting candidates
let bundledMessagesOfSameTemplate = this._getUnblockedMessages() let bundledMessagesOfSameTemplate = this._getUnblockedMessages()
@ -309,7 +315,7 @@ class _ASRouter {
} }
// Only copy the content of the message (that's what the UI cares about) // Only copy the content of the message (that's what the UI cares about)
// Also delete the message we picked so we don't pick it again // Also delete the message we picked so we don't pick it again
result.push({content: message.content, id: message.id}); result.push({content: message.content, id: message.id, order: message.order || 0});
bundledMessagesOfSameTemplate.splice(bundledMessagesOfSameTemplate.findIndex(msg => msg.id === message.id), 1); bundledMessagesOfSameTemplate.splice(bundledMessagesOfSameTemplate.findIndex(msg => msg.id === message.id), 1);
// Stop once we have enough messages to fill a bundle // Stop once we have enough messages to fill a bundle
if (result.length === originalMessage.bundled) { if (result.length === originalMessage.bundled) {
@ -322,7 +328,8 @@ class _ASRouter {
if (result.length < originalMessage.bundled) { if (result.length < originalMessage.bundled) {
return null; return null;
} }
return {bundle: result, provider: originalMessage.provider, template: originalMessage.template};
return {bundle: this._orderBundle(result), provider: originalMessage.provider, template: originalMessage.template};
} }
_getUnblockedMessages() { _getUnblockedMessages() {
@ -468,18 +475,42 @@ class _ASRouter {
_validPreviewEndpoint(url) { _validPreviewEndpoint(url) {
try { try {
const endpoint = new URL(url); const endpoint = new URL(url);
if (!WHITELIST_HOSTS[endpoint.host]) { if (!this.WHITELIST_HOSTS[endpoint.host]) {
Cu.reportError(`The preview URL host ${endpoint.host} is not in the whitelist.`); Cu.reportError(`The preview URL host ${endpoint.host} is not in the whitelist.`);
} }
if (endpoint.protocol !== "https:") { if (endpoint.protocol !== "https:") {
Cu.reportError("The URL protocol is not https."); Cu.reportError("The URL protocol is not https.");
} }
return (endpoint.protocol === "https:" && WHITELIST_HOSTS[endpoint.host]); return (endpoint.protocol === "https:" && this.WHITELIST_HOSTS[endpoint.host]);
} catch (e) { } catch (e) {
return false; return false;
} }
} }
_loadSnippetsWhitelistHosts() {
let additionalHosts = [];
const whitelistPrefValue = Services.prefs.getStringPref(SNIPPETS_ENDPOINT_WHITELIST, "");
try {
additionalHosts = JSON.parse(whitelistPrefValue);
} catch (e) {
if (whitelistPrefValue) {
Cu.reportError(`Pref ${SNIPPETS_ENDPOINT_WHITELIST} value is not valid JSON`);
}
}
if (!additionalHosts.length) {
return DEFAULT_WHITELIST_HOSTS;
}
// If there are additional hosts we want to whitelist, add them as
// `preview` so that the updateCycle is 0
return additionalHosts.reduce((whitelist_hosts, host) => {
whitelist_hosts[host] = "preview";
Services.console.logStringMessage(`Adding ${host} to whitelist hosts.`);
return whitelist_hosts;
}, {...DEFAULT_WHITELIST_HOSTS});
}
async _addPreviewEndpoint(url) { async _addPreviewEndpoint(url) {
const providers = [...this.state.providers]; const providers = [...this.state.providers];
if (this._validPreviewEndpoint(url) && !providers.find(p => p.url === url)) { if (this._validPreviewEndpoint(url) && !providers.find(p => p.url === url)) {

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

@ -40,6 +40,11 @@ const TopFrecentSitesCache = {
} }
resolve(this._topFrecentSites); resolve(this._topFrecentSites);
}); });
},
// For testing
expire() {
this._lastUpdated = 0;
this._topFrecentSites = null;
} }
}; };
@ -215,4 +220,6 @@ this.ASRouterTargeting = {
} }
}; };
this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "removeRandomItemFromArray"]; // Export for testing
this.TopFrecentSitesCache = TopFrecentSitesCache;
this.EXPORTED_SYMBOLS = ["ASRouterTargeting", "TopFrecentSitesCache"];

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

@ -5,6 +5,7 @@
"use strict"; "use strict";
ChromeUtils.import("resource:///modules/AboutNewTab.jsm"); ChromeUtils.import("resource:///modules/AboutNewTab.jsm");
/* globals RemotePages */ // Remove when updating eslint-plugin-mozilla 0.14.0+
ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm"); ChromeUtils.import("resource://gre/modules/remotepagemanager/RemotePageManagerParent.jsm");
const {actionCreators: ac, actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {}); const {actionCreators: ac, actionTypes: at, actionUtils: au} = ChromeUtils.import("resource://activity-stream/common/Actions.jsm", {});

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

@ -8,6 +8,7 @@ const ONBOARDING_MESSAGES = [
id: "ONBOARDING_1", id: "ONBOARDING_1",
template: "onboarding", template: "onboarding",
bundled: 3, bundled: 3,
order: 2,
content: { content: {
title: "Private Browsing", title: "Private Browsing",
text: "Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web.", text: "Browse by yourself. Private Browsing with Tracking Protection blocks online trackers that follow you around the web.",
@ -21,6 +22,7 @@ const ONBOARDING_MESSAGES = [
id: "ONBOARDING_2", id: "ONBOARDING_2",
template: "onboarding", template: "onboarding",
bundled: 3, bundled: 3,
order: 3,
content: { content: {
title: "Screenshots", title: "Screenshots",
text: "Take, save and share screenshots - without leaving Firefox. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.", text: "Take, save and share screenshots - without leaving Firefox. Capture a region or an entire page as you browse. Then save to the web for easy access and sharing.",
@ -35,6 +37,7 @@ const ONBOARDING_MESSAGES = [
id: "ONBOARDING_3", id: "ONBOARDING_3",
template: "onboarding", template: "onboarding",
bundled: 3, bundled: 3,
order: 1,
content: { content: {
title: "Add-ons", title: "Add-ons",
text: "Add even more features that make Firefox work harder for you. Compare prices, check the weather or express your personality with a custom theme.", text: "Add even more features that make Firefox work harder for you. Compare prices, check the weather or express your personality with a custom theme.",
@ -50,11 +53,12 @@ const ONBOARDING_MESSAGES = [
id: "ONBOARDING_4", id: "ONBOARDING_4",
template: "onboarding", template: "onboarding",
bundled: 3, bundled: 3,
order: 1,
content: { content: {
title: "Extensions", title: "Block Ads with Ghostery",
text: "Make browsing faster, smarter, or safer with browser apps. Protect passwords, find deals, download videos, and much more. You can even block annoying ads with extensions like Ghostery.", text: "Browse faster, smarter, or safer with extensions like Ghostery, which lets you block annoying ads.",
icon: "gift", icon: "gift",
button_label: "Get Ghostery", button_label: "Try It Now",
button_action: "OPEN_URL", button_action: "OPEN_URL",
button_action_params: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/" button_action_params: "https://addons.mozilla.org/en-US/firefox/addon/ghostery/"
}, },

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

@ -105,7 +105,7 @@ prefs_highlights_options_visited_label=Baxılmış Səhifələr
prefs_highlights_options_download_label=Son Endirmələr prefs_highlights_options_download_label=Son Endirmələr
prefs_highlights_options_pocket_label=Pocket-ə Saxlanılan Səhifələr prefs_highlights_options_pocket_label=Pocket-ə Saxlanılan Səhifələr
prefs_snippets_description=Mozilla və Firefoxdan yeniliklər prefs_snippets_description=Mozilla və Firefoxdan yeniliklər
settings_pane_button_label=Yeni Vərəq səhifənizi özəlləşdirin settings_pane_button_label=Yeni Vərəq səhifənizi fərdiləşdirin
settings_pane_topsites_header=Qabaqcıl Saytlar settings_pane_topsites_header=Qabaqcıl Saytlar
settings_pane_highlights_header=Seçilmişlər settings_pane_highlights_header=Seçilmişlər
settings_pane_highlights_options_bookmarks=Əlfəcinlər settings_pane_highlights_options_bookmarks=Əlfəcinlər

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

@ -206,6 +206,8 @@ firstrun_form_sub_header=for at fortsætte til Firefox Sync.
firstrun_email_input_placeholder=Mailadresse firstrun_email_input_placeholder=Mailadresse
firstrun_invalid_input=En gyldig mailadresse er påkrævet
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and # 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. # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Ved at fortsætte godkender du vores {terms} og {privacy}. firstrun_extra_legal_links=Ved at fortsætte godkender du vores {terms} og {privacy}.

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

@ -191,6 +191,7 @@ firstrun_form_sub_header=para acceder a Firefox Sync.
firstrun_email_input_placeholder=Correo electrónico firstrun_email_input_placeholder=Correo electrónico
firstrun_invalid_input=Se requiere un correo válido
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and # 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. # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.

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

@ -191,6 +191,8 @@ firstrun_form_sub_header=eike hag̃ua Firefox Sync-pe.
firstrun_email_input_placeholder=Ñandutiveve firstrun_email_input_placeholder=Ñandutiveve
firstrun_invalid_input=Eikotevẽ peteĩ ñanduti veve oikóva
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and # 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. # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Ejapóva, emoneĩ ko'ã {terms} ha {privacy}. firstrun_extra_legal_links=Ejapóva, emoneĩ ko'ã {terms} ha {privacy}.

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

@ -191,6 +191,8 @@ firstrun_form_sub_header=Firefox Sync の利用を続けるために必要です
firstrun_email_input_placeholder=メールアドレス firstrun_email_input_placeholder=メールアドレス
firstrun_invalid_input=メールアドレスを正しく入力してください
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and # 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. # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=続行すると、{terms} と {privacy} に同意したものとみなします。 firstrun_extra_legal_links=続行すると、{terms} と {privacy} に同意したものとみなします。

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

@ -191,6 +191,8 @@ firstrun_form_sub_header=Firefox Sync の利用を続けるために必要です
firstrun_email_input_placeholder=メールアドレス firstrun_email_input_placeholder=メールアドレス
firstrun_invalid_input=メールアドレスを正しく入力してください
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and # 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. # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=続行すると、{terms} と {privacy} に同意したものとみなします。 firstrun_extra_legal_links=続行すると、{terms} と {privacy} に同意したものとみなします。

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

@ -191,6 +191,8 @@ firstrun_form_sub_header=பயர்பாக்சு ஒத்திசைய
firstrun_email_input_placeholder=மின்னஞ்சல் firstrun_email_input_placeholder=மின்னஞ்சல்
firstrun_invalid_input=நம்பகரமான மின்னஞ்சல் தேவை
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and # 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. # {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=தொடர்வதன் மூலம், தாங்கள் {terms} மற்றும் {privacy} ஒப்புக்கொள்கின்றீர்கள். firstrun_extra_legal_links=தொடர்வதன் மூலம், தாங்கள் {terms} மற்றும் {privacy} ஒப்புக்கொள்கின்றீர்கள்.

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

@ -40,7 +40,7 @@ confirm_history_delete_p1=您確定要刪除此頁面的所有瀏覽紀錄?
# LOCALIZATION NOTE (confirm_history_delete_notice_p2): this string is displayed in # 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 # the same dialog as confirm_history_delete_p1. "This action" refers to deleting a
# page from history. # page from history.
confirm_history_delete_notice_p2=無法還原此操作 confirm_history_delete_notice_p2=此動作無法復原
menu_action_save_to_pocket=儲存至 Pocket menu_action_save_to_pocket=儲存至 Pocket
menu_action_delete_pocket=從 Pocket 刪除 menu_action_delete_pocket=從 Pocket 刪除
menu_action_archive_pocket=在 Pocket 裡封存 menu_action_archive_pocket=在 Pocket 裡封存
@ -155,7 +155,7 @@ topstories_empty_state=所有文章都讀完啦!晚點再來,{provider} 將
manual_migration_explanation2=試試將其他瀏覽器的書籤、瀏覽記錄與密碼匯入 Firefox。 manual_migration_explanation2=試試將其他瀏覽器的書籤、瀏覽記錄與密碼匯入 Firefox。
# LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the # LOCALIZATION NOTE (manual_migration_cancel_button): This message is shown on a button that cancels the
# process of importing another browsers profile into Firefox. # process of importing another browsers profile into Firefox.
manual_migration_cancel_button=必了 manual_migration_cancel_button=要,謝謝
# LOCALIZATION NOTE (manual_migration_import_button): This message is shown on a button that starts the process # 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. # of importing another browsers profile profile into Firefox.
manual_migration_import_button=立即匯入 manual_migration_import_button=立即匯入
@ -187,7 +187,7 @@ firstrun_learn_more_link=了解 Firefox Accounts 的更多資訊
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence. # 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 is displayed more boldly as the call to action.
firstrun_form_header=輸入您的電子郵件地址 firstrun_form_header=輸入您的電子郵件地址
firstrun_form_sub_header=繼續前往 Firefox Sync firstrun_form_sub_header=繼續前往 Firefox Sync
firstrun_email_input_placeholder=電子郵件 firstrun_email_input_placeholder=電子郵件

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

@ -15,7 +15,6 @@ cd /activity-stream && npm install . && npm run buildmc
cd /mozilla-central && ./mach build \ cd /mozilla-central && ./mach build \
&& ./mach test browser_parsable_css \ && ./mach test browser_parsable_css \
&& ./mach lint -l eslint -l codespell browser/components/newtab \ && ./mach lint -l eslint -l codespell browser/components/newtab \
&& ./mach test browser/components/newtab --headless \
&& ./mach test browser/components/newtab/test/browser --headless \ && ./mach test browser/components/newtab/test/browser --headless \
&& ./mach test browser/components/newtab/test/xpcshell \ && ./mach test browser/components/newtab/test/xpcshell \
&& ./mach test browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js --headless \ && ./mach test browser/components/preferences/in-content/tests/browser_hometab_restore_defaults.js --headless \

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

@ -90,27 +90,26 @@
"mc_dir": "../mozilla-central" "mc_dir": "../mozilla-central"
}, },
"scripts": { "scripts": {
"mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless)", "mochitest": "(cd $npm_package_config_mc_dir && ./mach mochitest browser/components/newtab/test/browser --headless)",
"mochitest-debug": "(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest)", "mochitest-debug": "(cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/components/newtab/test/browser)",
"bundle": "npm-run-all bundle:*", "bundle": "npm-run-all bundle:*",
"bundle:locales": "pontoon-to-json --src locales --dest data", "bundle:locales": "pontoon-to-json --src locales --dest data",
"bundle:webpack": "webpack --config webpack.system-addon.config.js", "bundle:webpack": "webpack --config webpack.system-addon.config.js",
"bundle:css": "node-sass --source-map true --source-map-contents content-src/styles -o css", "bundle:css": "node-sass --source-map true --source-map-contents content-src/styles -o css",
"bundle:html": "rimraf prerendered && webpack --config webpack.prerender.config.js && node ./bin/render-activity-stream-html.js", "bundle:html": "rimraf prerendered && webpack --config webpack.prerender.config.js && node ./bin/render-activity-stream-html.js",
"buildmc": "npm-run-all buildmc:*", "buildmc": "npm-run-all buildmc:*",
"prebuildmc": "rimraf $npm_package_config_mc_dir/browser/extensions/activity-stream/", "prebuildmc": "rimraf $npm_package_config_mc_dir/browser/components/newtab/",
"buildmc:bundle": "npm run bundle", "buildmc:bundle": "npm run bundle",
"buildmc:copy": "rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/extensions/activity-stream/", "buildmc:copy": "rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/components/newtab/",
"buildmc:version": "node ./bin/update-version.js $npm_package_config_mc_dir/browser/extensions/activity-stream",
"buildmc:stringsExport": "cp locales/en-US/strings.properties $npm_package_config_mc_dir/browser/locales/en-US/chrome/browser/activity-stream/newtab.properties", "buildmc:stringsExport": "cp locales/en-US/strings.properties $npm_package_config_mc_dir/browser/locales/en-US/chrome/browser/activity-stream/newtab.properties",
"buildmc:copyPingCentre": "cpx \"ping-centre/PingCentre.jsm\" $npm_package_config_mc_dir/browser/modules", "buildmc:copyPingCentre": "cpx \"ping-centre/PingCentre.jsm\" $npm_package_config_mc_dir/browser/modules",
"startmc": "npm-run-all --parallel startmc:*", "startmc": "npm-run-all --parallel startmc:*",
"prestartmc": "npm run buildmc", "prestartmc": "npm run buildmc",
"startmc:copy": "cpx \"{{,.}*,!(node_modules)/**/{,.}*}\" $npm_package_config_mc_dir/browser/extensions/activity-stream/ -w", "startmc:copy": "cpx \"{{,.}*,!(node_modules)/**/{,.}*}\" $npm_package_config_mc_dir/browser/components/newtab/ -w",
"startmc:copyPingCentre": "npm run buildmc:copyPingCentre -- -w", "startmc:copyPingCentre": "npm run buildmc:copyPingCentre -- -w",
"startmc:webpack": "npm run bundle:webpack -- -w", "startmc:webpack": "npm run bundle:webpack -- -w",
"startmc:css": "npm run bundle:css && npm run bundle:css -- -w", "startmc:css": "npm run bundle:css && npm run bundle:css -- -w",
"importmc": "rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/extensions/activity-stream/ .", "importmc": "rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .",
"testmc": "npm-run-all testmc:*", "testmc": "npm-run-all testmc:*",
"testmc:lint": "npm run lint", "testmc:lint": "npm run lint",
"testmc:build": "npm run bundle:webpack && npm run bundle:locales", "testmc:build": "npm run bundle:webpack && npm run bundle:locales",

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

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

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

@ -51,7 +51,7 @@ window.gActivityStreamStrings = {
"prefs_highlights_options_download_label": "Son Endirmələr", "prefs_highlights_options_download_label": "Son Endirmələr",
"prefs_highlights_options_pocket_label": "Pocket-ə Saxlanılan Səhifələr", "prefs_highlights_options_pocket_label": "Pocket-ə Saxlanılan Səhifələr",
"prefs_snippets_description": "Mozilla və Firefoxdan yeniliklər", "prefs_snippets_description": "Mozilla və Firefoxdan yeniliklər",
"settings_pane_button_label": "Yeni Vərəq səhifənizi özəlləşdirin", "settings_pane_button_label": "Yeni Vərəq səhifənizi fərdiləşdirin",
"settings_pane_topsites_header": "Qabaqcıl Saytlar", "settings_pane_topsites_header": "Qabaqcıl Saytlar",
"settings_pane_highlights_header": "Seçilmişlər", "settings_pane_highlights_header": "Seçilmişlər",
"settings_pane_highlights_options_bookmarks": "Əlfəcinlər", "settings_pane_highlights_options_bookmarks": "Əlfəcinlər",

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

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Indtast din mailadresse", "firstrun_form_header": "Indtast din mailadresse",
"firstrun_form_sub_header": "for at fortsætte til Firefox Sync.", "firstrun_form_sub_header": "for at fortsætte til Firefox Sync.",
"firstrun_email_input_placeholder": "Mailadresse", "firstrun_email_input_placeholder": "Mailadresse",
"firstrun_invalid_input": "Valid email required", "firstrun_invalid_input": "En gyldig mailadresse er påkrævet",
"firstrun_extra_legal_links": "Ved at fortsætte godkender du vores {terms} og {privacy}.", "firstrun_extra_legal_links": "Ved at fortsætte godkender du vores {terms} og {privacy}.",
"firstrun_terms_of_service": "tjenestevilkår", "firstrun_terms_of_service": "tjenestevilkår",
"firstrun_privacy_notice": "privatlivspolitik", "firstrun_privacy_notice": "privatlivspolitik",

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

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "Emoinge ne ñandutiveve", "firstrun_form_header": "Emoinge ne ñandutiveve",
"firstrun_form_sub_header": "eike hag̃ua Firefox Sync-pe.", "firstrun_form_sub_header": "eike hag̃ua Firefox Sync-pe.",
"firstrun_email_input_placeholder": "Ñandutiveve", "firstrun_email_input_placeholder": "Ñandutiveve",
"firstrun_invalid_input": "Valid email required", "firstrun_invalid_input": "Eikotevẽ peteĩ ñanduti veve oikóva",
"firstrun_extra_legal_links": "Ejapóva, emoneĩ ko'ã {terms} ha {privacy}.", "firstrun_extra_legal_links": "Ejapóva, emoneĩ ko'ã {terms} ha {privacy}.",
"firstrun_terms_of_service": "Mba'epytyvõrã ñemboguata", "firstrun_terms_of_service": "Mba'epytyvõrã ñemboguata",
"firstrun_privacy_notice": "Ñemigua purureko", "firstrun_privacy_notice": "Ñemigua purureko",

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

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "メールアドレスを入力してください", "firstrun_form_header": "メールアドレスを入力してください",
"firstrun_form_sub_header": "Firefox Sync の利用を続けるために必要です", "firstrun_form_sub_header": "Firefox Sync の利用を続けるために必要です",
"firstrun_email_input_placeholder": "メールアドレス", "firstrun_email_input_placeholder": "メールアドレス",
"firstrun_invalid_input": "Valid email required", "firstrun_invalid_input": "メールアドレスを正しく入力してください",
"firstrun_extra_legal_links": "続行すると、{terms} と {privacy} に同意したものとみなします。", "firstrun_extra_legal_links": "続行すると、{terms} と {privacy} に同意したものとみなします。",
"firstrun_terms_of_service": "サービス利用規約", "firstrun_terms_of_service": "サービス利用規約",
"firstrun_privacy_notice": "プライバシーに関する通知", "firstrun_privacy_notice": "プライバシーに関する通知",

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

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "メールアドレスを入力してください", "firstrun_form_header": "メールアドレスを入力してください",
"firstrun_form_sub_header": "Firefox Sync の利用を続けるために必要です", "firstrun_form_sub_header": "Firefox Sync の利用を続けるために必要です",
"firstrun_email_input_placeholder": "メールアドレス", "firstrun_email_input_placeholder": "メールアドレス",
"firstrun_invalid_input": "Valid email required", "firstrun_invalid_input": "メールアドレスを正しく入力してください",
"firstrun_extra_legal_links": "続行すると、{terms} と {privacy} に同意したものとみなします。", "firstrun_extra_legal_links": "続行すると、{terms} と {privacy} に同意したものとみなします。",
"firstrun_terms_of_service": "サービス利用規約", "firstrun_terms_of_service": "サービス利用規約",
"firstrun_privacy_notice": "プライバシーに関する通知", "firstrun_privacy_notice": "プライバシーに関する通知",

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

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "உங்களின் மின்னஞ்சலை உள்ளிடுக", "firstrun_form_header": "உங்களின் மின்னஞ்சலை உள்ளிடுக",
"firstrun_form_sub_header": "பயர்பாக்சு ஒத்திசையைத் தொடர.", "firstrun_form_sub_header": "பயர்பாக்சு ஒத்திசையைத் தொடர.",
"firstrun_email_input_placeholder": "மின்னஞ்சல்", "firstrun_email_input_placeholder": "மின்னஞ்சல்",
"firstrun_invalid_input": "Valid email required", "firstrun_invalid_input": "நம்பகரமான மின்னஞ்சல் தேவை",
"firstrun_extra_legal_links": "தொடர்வதன் மூலம், தாங்கள் {terms} மற்றும் {privacy} ஒப்புக்கொள்கின்றீர்கள்.", "firstrun_extra_legal_links": "தொடர்வதன் மூலம், தாங்கள் {terms} மற்றும் {privacy} ஒப்புக்கொள்கின்றீர்கள்.",
"firstrun_terms_of_service": "சேவையின் விதிமுறைகள்", "firstrun_terms_of_service": "சேவையின் விதிமுறைகள்",
"firstrun_privacy_notice": "தனியுரிமை அறிவிப்பு", "firstrun_privacy_notice": "தனியுரிமை அறிவிப்பு",

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

@ -20,7 +20,7 @@ window.gActivityStreamStrings = {
"menu_action_pin": "釘選", "menu_action_pin": "釘選",
"menu_action_unpin": "取消釘選", "menu_action_unpin": "取消釘選",
"confirm_history_delete_p1": "您確定要刪除此頁面的所有瀏覽紀錄?", "confirm_history_delete_p1": "您確定要刪除此頁面的所有瀏覽紀錄?",
"confirm_history_delete_notice_p2": "無法還原此操作。", "confirm_history_delete_notice_p2": "此動作無法復原。",
"menu_action_save_to_pocket": "儲存至 Pocket", "menu_action_save_to_pocket": "儲存至 Pocket",
"menu_action_delete_pocket": "從 Pocket 刪除", "menu_action_delete_pocket": "從 Pocket 刪除",
"menu_action_archive_pocket": "在 Pocket 裡封存", "menu_action_archive_pocket": "在 Pocket 裡封存",
@ -77,7 +77,7 @@ window.gActivityStreamStrings = {
"highlights_empty_state": "開始上網,我們就會把您在網路上發現的好文章、影片、剛加入書籤的頁面顯示於此。", "highlights_empty_state": "開始上網,我們就會把您在網路上發現的好文章、影片、剛加入書籤的頁面顯示於此。",
"topstories_empty_state": "所有文章都讀完啦!晚點再來,{provider} 將提供更多推薦故事。等不及了?選擇熱門主題,看看 Web 上各式精采資訊。", "topstories_empty_state": "所有文章都讀完啦!晚點再來,{provider} 將提供更多推薦故事。等不及了?選擇熱門主題,看看 Web 上各式精采資訊。",
"manual_migration_explanation2": "試試將其他瀏覽器的書籤、瀏覽記錄與密碼匯入 Firefox。", "manual_migration_explanation2": "試試將其他瀏覽器的書籤、瀏覽記錄與密碼匯入 Firefox。",
"manual_migration_cancel_button": "不必了", "manual_migration_cancel_button": "不要,謝謝",
"manual_migration_import_button": "立即匯入", "manual_migration_import_button": "立即匯入",
"error_fallback_default_info": "唉唷,載入內容時發生錯誤。", "error_fallback_default_info": "唉唷,載入內容時發生錯誤。",
"error_fallback_default_refresh_suggestion": "請重新整理頁面再試一次。", "error_fallback_default_refresh_suggestion": "請重新整理頁面再試一次。",
@ -94,7 +94,7 @@ window.gActivityStreamStrings = {
"firstrun_content": "在您的任何裝置上取得書籤、瀏覽紀錄、密碼及其他設定。", "firstrun_content": "在您的任何裝置上取得書籤、瀏覽紀錄、密碼及其他設定。",
"firstrun_learn_more_link": "了解 Firefox Accounts 的更多資訊", "firstrun_learn_more_link": "了解 Firefox Accounts 的更多資訊",
"firstrun_form_header": "輸入您的電子郵件地址", "firstrun_form_header": "輸入您的電子郵件地址",
"firstrun_form_sub_header": "繼續前往 Firefox Sync", "firstrun_form_sub_header": "繼續前往 Firefox Sync",
"firstrun_email_input_placeholder": "電子郵件", "firstrun_email_input_placeholder": "電子郵件",
"firstrun_invalid_input": "必須輸入有效的電子郵件地址", "firstrun_invalid_input": "必須輸入有效的電子郵件地址",
"firstrun_extra_legal_links": "若繼續,代表您同意{terms}及{privacy}。", "firstrun_extra_legal_links": "若繼續,代表您同意{terms}及{privacy}。",

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

@ -1,5 +1,7 @@
ChromeUtils.defineModuleGetter(this, "ASRouterTargeting", const {ASRouterTargeting, TopFrecentSitesCache} =
"resource://activity-stream/lib/ASRouterTargeting.jsm"); ChromeUtils.import("resource://activity-stream/lib/ASRouterTargeting.jsm", {});
const {AddonTestUtils} =
ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
ChromeUtils.defineModuleGetter(this, "ProfileAge", ChromeUtils.defineModuleGetter(this, "ProfileAge",
"resource://gre/modules/ProfileAge.jsm"); "resource://gre/modules/ProfileAge.jsm");
ChromeUtils.defineModuleGetter(this, "AddonManager", ChromeUtils.defineModuleGetter(this, "AddonManager",
@ -11,8 +13,6 @@ ChromeUtils.defineModuleGetter(this, "NewTabUtils",
ChromeUtils.defineModuleGetter(this, "PlacesTestUtils", ChromeUtils.defineModuleGetter(this, "PlacesTestUtils",
"resource://testing-common/PlacesTestUtils.jsm"); "resource://testing-common/PlacesTestUtils.jsm");
const {AddonTestUtils} = ChromeUtils.import("resource://testing-common/AddonTestUtils.jsm", {});
// ASRouterTargeting.isMatch // ASRouterTargeting.isMatch
add_task(async function should_do_correct_targeting() { add_task(async function should_do_correct_targeting() {
is(await ASRouterTargeting.isMatch("FOO", {}, {FOO: true}), true, "should return true for a matching value"); is(await ASRouterTargeting.isMatch("FOO", {}, {FOO: true}), true, "should return true for a matching value");
@ -226,4 +226,8 @@ add_task(async function checkFrecentSites() {
message = {id: "foo", targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`}; message = {id: "foo", targeting: `(topFrecentSites[.frecency >= 900 && .lastVisitDate >= ${timeDaysAgo(1) - 1}]|mapToProperty('host') intersect ['mozilla3.com', 'mozilla2.com', 'mozilla1.com'])|length > 0`};
is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message, is(await ASRouterTargeting.findMatchingMessage({messages: [message], target: {}}), message,
"should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains"); "should select correct item when filtering by frecency and lastVisitDate with multiple candidate domains");
// Cleanup
await clearHistoryAndBookmarks();
TopFrecentSitesCache.expire();
}); });

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

@ -7,10 +7,17 @@ XPCOMUtils.defineLazyServiceGetter(this, "aboutNewTabService",
"@mozilla.org/browser/aboutnewtab-service;1", "@mozilla.org/browser/aboutnewtab-service;1",
"nsIAboutNewTabService"); "nsIAboutNewTabService");
registerCleanupFunction(function() { registerCleanupFunction(() => {
aboutNewTabService.resetNewTabURL(); aboutNewTabService.resetNewTabURL();
}); });
function nextChangeNotificationPromise(aNewURL, testMessage) {
return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
Assert.equal(aData, aNewURL, testMessage);
return true;
});
}
/* /*
* Tests that the default newtab page is always returned when one types "about:newtab" in the URL bar, * Tests that the default newtab page is always returned when one types "about:newtab" in the URL bar,
* even when overridden. * even when overridden.
@ -18,7 +25,7 @@ registerCleanupFunction(function() {
add_task(async function redirector_ignores_override() { add_task(async function redirector_ignores_override() {
let overrides = [ let overrides = [
"chrome://browser/content/aboutRobots.xhtml", "chrome://browser/content/aboutRobots.xhtml",
"about:home", "about:home"
]; ];
for (let overrideURL of overrides) { for (let overrideURL of overrides) {
@ -30,7 +37,7 @@ add_task(async function redirector_ignores_override() {
let tabOptions = { let tabOptions = {
gBrowser, gBrowser,
url: "about:newtab", url: "about:newtab"
}; };
/* /*
@ -40,8 +47,8 @@ add_task(async function redirector_ignores_override() {
* due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead * due to invoking AboutRedirector. A user interacting with the chrome otherwise would lead
* to the overriding URLs. * to the overriding URLs.
*/ */
await BrowserTestUtils.withNewTab(tabOptions, async function(browser) { await BrowserTestUtils.withNewTab(tabOptions, async browser => {
await ContentTask.spawn(browser, {}, async function() { await ContentTask.spawn(browser, {}, async () => {
Assert.equal(content.location.href, "about:newtab", "Got right URL"); Assert.equal(content.location.href, "about:newtab", "Got right URL");
Assert.equal(content.document.location.href, "about:newtab", "Got right URL"); Assert.equal(content.document.location.href, "about:newtab", "Got right URL");
Assert.notEqual(content.document.nodePrincipal, Assert.notEqual(content.document.nodePrincipal,
@ -59,7 +66,7 @@ add_task(async function override_loads_in_browser() {
let overrides = [ let overrides = [
"chrome://browser/content/aboutRobots.xhtml", "chrome://browser/content/aboutRobots.xhtml",
"about:home", "about:home",
" about:home", " about:home"
]; ];
for (let overrideURL of overrides) { for (let overrideURL of overrides) {
@ -75,7 +82,7 @@ add_task(async function override_loads_in_browser() {
let browser = gBrowser.selectedBrowser; let browser = gBrowser.selectedBrowser;
await BrowserTestUtils.browserLoaded(browser); await BrowserTestUtils.browserLoaded(browser);
await ContentTask.spawn(browser, {url: overrideURL}, async function(args) { await ContentTask.spawn(browser, {url: overrideURL}, async args => {
Assert.equal(content.location.href, args.url.trim(), "Got right URL"); Assert.equal(content.location.href, args.url.trim(), "Got right URL");
Assert.equal(content.document.location.href, args.url.trim(), "Got right URL"); Assert.equal(content.document.location.href, args.url.trim(), "Got right URL");
}); // jshint ignore:line }); // jshint ignore:line
@ -91,7 +98,7 @@ add_task(async function override_blank_loads_in_browser() {
"", "",
" ", " ",
"\n\t", "\n\t",
" about:blank", " about:blank"
]; ];
for (let overrideURL of overrides) { for (let overrideURL of overrides) {
@ -107,17 +114,10 @@ add_task(async function override_blank_loads_in_browser() {
let browser = gBrowser.selectedBrowser; let browser = gBrowser.selectedBrowser;
await BrowserTestUtils.browserLoaded(browser); await BrowserTestUtils.browserLoaded(browser);
await ContentTask.spawn(browser, {}, async function() { await ContentTask.spawn(browser, {}, async () => {
Assert.equal(content.location.href, "about:blank", "Got right URL"); Assert.equal(content.location.href, "about:blank", "Got right URL");
Assert.equal(content.document.location.href, "about:blank", "Got right URL"); Assert.equal(content.document.location.href, "about:blank", "Got right URL");
}); // jshint ignore:line }); // jshint ignore:line
BrowserTestUtils.removeTab(gBrowser.selectedTab); BrowserTestUtils.removeTab(gBrowser.selectedTab);
} }
}); });
function nextChangeNotificationPromise(aNewURL, testMessage) {
return TestUtils.topicObserved("newtab-url-changed", function observer(aSubject, aData) { // jshint unused:false
Assert.equal(aData, aNewURL, testMessage);
return true;
});
}

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

@ -59,7 +59,7 @@ add_task(async function test_all_packaged_locales() {
const locale = file.replace("/", ""); const locale = file.replace("/", "");
if (locale !== "static") { if (locale !== "static") {
const url = await getUrlForLocale(locale); const url = await getUrlForLocale(locale);
Assert[locale === "en-US" ? "equal" : "notEqual"](url, DEFAULT_URL, `can reference "${locale}" files`); Assert.equal(url, DEFAULT_URL.replace("en-US", locale), `can reference "${locale}" files`);
// Specially remember if we saw an ID locale packaged as it can be // Specially remember if we saw an ID locale packaged as it can be
// easily ignored by source control, e.g., .gitignore // easily ignored by source control, e.g., .gitignore

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

@ -128,6 +128,22 @@ describe("ASRouter", () => {
assert.lengthOf(Router.state.providers, length); assert.lengthOf(Router.state.providers, length);
assert.isDefined(provider); assert.isDefined(provider);
}); });
it("should load additional whitelisted hosts", async () => {
getStringPrefStub.returns("[\"whitelist.com\"]");
await createRouterAndInit();
assert.propertyVal(Router.WHITELIST_HOSTS, "whitelist.com", "preview");
// Should still include the defaults
assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 3);
});
it("should fallback to defaults if pref parsing fails", async () => {
getStringPrefStub.returns("err");
await createRouterAndInit();
assert.lengthOf(Object.keys(Router.WHITELIST_HOSTS), 2);
assert.propertyVal(Router.WHITELIST_HOSTS, "snippets-admin.mozilla.org", "preview");
assert.propertyVal(Router.WHITELIST_HOSTS, "activity-stream-icons.services.mozilla.com", "production");
});
}); });
describe("#loadMessagesFromAllProviders", () => { describe("#loadMessagesFromAllProviders", () => {
@ -143,8 +159,10 @@ describe("ASRouter", () => {
getStringPrefStub.returns("example.com"); getStringPrefStub.returns("example.com");
await createRouterAndInit(); await createRouterAndInit();
assert.calledOnce(getStringPrefStub); // Get snippets endpoint url, get the whitelisted hosts for endpoints
assert.calledTwice(getStringPrefStub);
assert.calledWithExactly(getStringPrefStub, "remotePref", ""); assert.calledWithExactly(getStringPrefStub, "remotePref", "");
assert.calledWithExactly(getStringPrefStub, "browser.newtab.activity-stream.asrouter.whitelistHosts", "");
assert.isDefined(Router.state.providers.find(p => p.url === "example.com")); assert.isDefined(Router.state.providers.find(p => p.url === "example.com"));
}); });
it("should not trigger an update if not enough time has passed for a provider", async () => { it("should not trigger an update if not enough time has passed for a provider", async () => {
@ -248,6 +266,18 @@ describe("ASRouter", () => {
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES"); assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].type, "SET_BUNDLED_MESSAGES");
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content); assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, currentMessage.content);
}); });
it("should properly order the message's bundle if specified", async () => {
// force the only messages to be a bundled messages so getRandomItemFromArray picks one of them
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"});
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");
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[0].content, firstMessage.content);
assert.equal(msg.target.sendAsyncMessage.firstCall.args[1].data.bundle[1].content, secondMessage.content);
});
it("should return a null bundle if we do not have enough messages to fill the bundle", async () => { it("should return a null bundle if we do not have enough messages to fill the bundle", async () => {
// force the only message to be a bundled message that needs 2 messages in the bundle // 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"}}]}); await Router.setState({messages: [{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}]});
@ -419,7 +449,7 @@ describe("ASRouter", () => {
const expectedObj = { const expectedObj = {
template: testMessage1.template, template: testMessage1.template,
provider: testMessage1.provider, provider: testMessage1.provider,
bundle: [{content: testMessage1.content, id: testMessage1.id}, {content: testMessage2.content, id: testMessage2.id}] bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id}]
}; };
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj}); assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
}); });
@ -434,7 +464,7 @@ describe("ASRouter", () => {
const expectedObj = { const expectedObj = {
template: testMessage1.template, template: testMessage1.template,
provider: testMessage1.provider, provider: testMessage1.provider,
bundle: [{content: testMessage1.content, id: testMessage1.id}, {content: testMessage2.content, id: testMessage2.id}] bundle: [{content: testMessage1.content, id: testMessage1.id, order: 1}, {content: testMessage2.content, id: testMessage2.id, order: 2}]
}; };
assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj}); assert.calledWith(msg.target.sendAsyncMessage, PARENT_TO_CHILD_MESSAGE_NAME, {type: "SET_BUNDLED_MESSAGES", data: expectedObj});
}); });

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

@ -4,8 +4,8 @@ export const EXPERIMENT_PREF = "asrouterExperimentEnabled";
export const FAKE_LOCAL_MESSAGES = [ export const FAKE_LOCAL_MESSAGES = [
{id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}}, {id: "foo", template: "simple_template", content: {title: "Foo", body: "Foo123"}},
{id: "foo1", template: "simple_template", bundled: 2, content: {title: "Foo1", body: "Foo123-1"}}, {id: "foo1", template: "simple_template", bundled: 2, order: 1, content: {title: "Foo1", body: "Foo123-1"}},
{id: "foo2", template: "simple_template", bundled: 2, content: {title: "Foo2", body: "Foo123-2"}}, {id: "foo2", template: "simple_template", bundled: 2, order: 2, content: {title: "Foo2", body: "Foo123-2"}},
{id: "bar", template: "fancy_template", content: {title: "Foo", body: "Foo123"}}, {id: "bar", template: "fancy_template", content: {title: "Foo", body: "Foo123"}},
{id: "baz", content: {title: "Foo", body: "Foo123"}} {id: "baz", content: {title: "Foo", body: "Foo123"}}
]; ];

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

@ -31,6 +31,38 @@ function cleanup() {
registerCleanupFunction(cleanup); registerCleanupFunction(cleanup);
function nextChangeNotificationPromise(aNewURL, testMessage) {
return new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
Services.obs.removeObserver(observer, aTopic);
Assert.equal(aData, aNewURL, testMessage);
resolve();
}, "newtab-url-changed");
});
}
function setBoolPrefAndWaitForChange(pref, value, testMessage) {
return new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
Services.obs.removeObserver(observer, aTopic);
Assert.equal(aData, aboutNewTabService.newTabURL, testMessage);
resolve();
}, "newtab-url-changed");
Services.prefs.setBoolPref(pref, value);
});
}
function setupASPrerendered() {
if (Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF)) {
return Promise.resolve();
}
let notificationPromise = nextChangeNotificationPromise("about:newtab");
Services.prefs.setBoolPref(ACTIVITY_STREAM_PRERENDER_PREF, true);
return notificationPromise;
}
add_task(async function test_as_and_prerender_initialized() { add_task(async function test_as_and_prerender_initialized() {
Assert.ok(aboutNewTabService.activityStreamEnabled, Assert.ok(aboutNewTabService.activityStreamEnabled,
".activityStreamEnabled should be set to the correct initial value"); ".activityStreamEnabled should be set to the correct initial value");
@ -165,10 +197,8 @@ add_task(function test_locale() {
* Tests reponse to updates to prefs * Tests reponse to updates to prefs
*/ */
add_task(async function test_updates() { add_task(async function test_updates() {
/* // Simulates a "cold-boot" situation, with some pref already set before testing a series
* Simulates a "cold-boot" situation, with some pref already set before testing a series // of changes.
* of changes.
*/
await setupASPrerendered(); await setupASPrerendered();
aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off aboutNewTabService.resetNewTabURL(); // need to set manually because pref notifs are off
@ -197,36 +227,3 @@ add_task(async function test_updates() {
cleanup(); cleanup();
}); });
function nextChangeNotificationPromise(aNewURL, testMessage) {
return new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
Services.obs.removeObserver(observer, aTopic);
Assert.equal(aData, aNewURL, testMessage);
resolve();
}, "newtab-url-changed");
});
}
function setBoolPrefAndWaitForChange(pref, value, testMessage) {
return new Promise(resolve => {
Services.obs.addObserver(function observer(aSubject, aTopic, aData) { // jshint unused:false
Services.obs.removeObserver(observer, aTopic);
Assert.equal(aData, aboutNewTabService.newTabURL, testMessage);
resolve();
}, "newtab-url-changed");
Services.prefs.setBoolPref(pref, value);
});
}
function setupASPrerendered() {
if (Services.prefs.getBoolPref(ACTIVITY_STREAM_PRERENDER_PREF)) {
return Promise.resolve();
}
let notificationPromise = nextChangeNotificationPromise("about:newtab");
Services.prefs.setBoolPref(ACTIVITY_STREAM_PRERENDER_PREF, true);
return notificationPromise;
}

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

@ -5,12 +5,12 @@
scripts: scripts:
# Run the activity-stream mochitests # Run the activity-stream mochitests
mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/extensions/activity-stream/test/functional/mochitest --headless) mochitest: (cd $npm_package_config_mc_dir && ./mach mochitest browser/components/newtab/test/browser --headless)
# Run the activity-stream mochitests with the browser toolbox debugger. # Run the activity-stream mochitests with the browser toolbox debugger.
# Often handy in combination with adding a "debugger" statement in your # Often handy in combination with adding a "debugger" statement in your
# mochitest somewhere. # mochitest somewhere.
mochitest-debug: (cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/extensions/activity-stream/test/functional/mochitest) mochitest-debug: (cd $npm_package_config_mc_dir && ./mach mochitest --jsdebugger browser/components/newtab/test/browser)
# bundle: Build all assets for activity stream # bundle: Build all assets for activity stream
bundle: bundle:
@ -21,10 +21,9 @@ scripts:
# buildmc: Export the bootstraped add-on to mozilla central # buildmc: Export the bootstraped add-on to mozilla central
buildmc: buildmc:
pre: rimraf $npm_package_config_mc_dir/browser/extensions/activity-stream/ pre: rimraf $npm_package_config_mc_dir/browser/components/newtab/
bundle: => bundle bundle: => bundle
copy: rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/extensions/activity-stream/ copy: rsync --exclude-from .mcignore -a . $npm_package_config_mc_dir/browser/components/newtab/
version: node ./bin/update-version.js $npm_package_config_mc_dir/browser/extensions/activity-stream
stringsExport: cp locales/en-US/strings.properties $npm_package_config_mc_dir/browser/locales/en-US/chrome/browser/activity-stream/newtab.properties stringsExport: cp locales/en-US/strings.properties $npm_package_config_mc_dir/browser/locales/en-US/chrome/browser/activity-stream/newtab.properties
copyPingCentre: cpx "ping-centre/PingCentre.jsm" $npm_package_config_mc_dir/browser/modules copyPingCentre: cpx "ping-centre/PingCentre.jsm" $npm_package_config_mc_dir/browser/modules
@ -33,13 +32,13 @@ scripts:
_parallel: true _parallel: true
pre: =>buildmc pre: =>buildmc
# This copies only the system addon sub-folder; changing anything outside of it will need a full rebuild. # This copies only the system addon sub-folder; changing anything outside of it will need a full rebuild.
copy: cpx "{{,.}*,!(node_modules)/**/{,.}*}" $npm_package_config_mc_dir/browser/extensions/activity-stream/ -w copy: cpx "{{,.}*,!(node_modules)/**/{,.}*}" $npm_package_config_mc_dir/browser/components/newtab/ -w
copyPingCentre: =>buildmc:copyPingCentre -- -w copyPingCentre: =>buildmc:copyPingCentre -- -w
webpack: =>bundle:webpack -- -w webpack: =>bundle:webpack -- -w
css: =>bundle:css && =>bundle:css -- -w css: =>bundle:css && =>bundle:css -- -w
# importmc: Import changes from mc to github repo # importmc: Import changes from mc to github repo
importmc: rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/extensions/activity-stream/ . importmc: rsync --exclude-from .mcignore -a $npm_package_config_mc_dir/browser/components/newtab/ .
testmc: testmc:
lint: =>lint lint: =>lint