Bug 1481559 - Add search filter, search pref and bug fixes to Activity Stream r=ursula

MozReview-Commit-ID: ANMt3NGC8HY

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

--HG--
extra : moz-landing-system : lando
This commit is contained in:
Ed Lee 2018-08-07 18:55:31 +00:00
parent 21e6277fe3
commit 613dc273cb
31 changed files with 267 additions and 133 deletions

View File

@ -169,7 +169,7 @@ export const LinkMenuOptions = {
}),
SaveToPocket: (site, index, eventSource) => ({
id: "menu_action_save_to_pocket",
icon: "pocket",
icon: "pocket-save",
action: ac.AlsoToMain({
type: at.SAVE_TO_POCKET,
data: {site: {url: site.url, title: site.title}}

View File

@ -89,6 +89,10 @@
background-image: url('#{$image-path}glyph-pocket-16.svg');
}
&.icon-pocket-save {
background-image: url('#{$image-path}glyph-pocket-save-16.svg');
}
&.icon-history-item {
background-image: url('chrome://browser/skin/history.svg');
}

View File

@ -164,6 +164,8 @@ body {
background-image: url("../data/content/assets/glyph-edit-16.svg"); }
.icon.icon-pocket {
background-image: url("../data/content/assets/glyph-pocket-16.svg"); }
.icon.icon-pocket-save {
background-image: url("../data/content/assets/glyph-pocket-save-16.svg"); }
.icon.icon-history-item {
background-image: url("chrome://browser/skin/history.svg"); }
.icon.icon-trending {

File diff suppressed because one or more lines are too long

View File

@ -167,6 +167,8 @@ body {
background-image: url("../data/content/assets/glyph-edit-16.svg"); }
.icon.icon-pocket {
background-image: url("../data/content/assets/glyph-pocket-16.svg"); }
.icon.icon-pocket-save {
background-image: url("../data/content/assets/glyph-pocket-save-16.svg"); }
.icon.icon-history-item {
background-image: url("chrome://browser/skin/history.svg"); }
.icon.icon-trending {

File diff suppressed because one or more lines are too long

View File

@ -164,6 +164,8 @@ body {
background-image: url("../data/content/assets/glyph-edit-16.svg"); }
.icon.icon-pocket {
background-image: url("../data/content/assets/glyph-pocket-16.svg"); }
.icon.icon-pocket-save {
background-image: url("../data/content/assets/glyph-pocket-save-16.svg"); }
.icon.icon-history-item {
background-image: url("chrome://browser/skin/history.svg"); }
.icon.icon-trending {

File diff suppressed because one or more lines are too long

View File

@ -2850,7 +2850,7 @@ const LinkMenuOptions = {
}),
SaveToPocket: (site, index, eventSource) => ({
id: "menu_action_save_to_pocket",
icon: "pocket",
icon: "pocket-save",
action: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionCreators"].AlsoToMain({
type: common_Actions_jsm__WEBPACK_IMPORTED_MODULE_0__["actionTypes"].SAVE_TO_POCKET,
data: { site: { url: site.url, title: site.title } }

File diff suppressed because one or more lines are too long

View File

@ -1,6 +1 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" viewBox="0 0 16 16">
<path fill-opacity="context-fill-opacity" fill="context-fill" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/>
</svg>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" d="M8 15a8 8 0 0 1-8-8V3a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v4a8 8 0 0 1-8 8zm3.985-10.032a.99.99 0 0 0-.725.319L7.978 8.57 4.755 5.336A.984.984 0 0 0 4 4.968a1 1 0 0 0-.714 1.7l-.016.011 3.293 3.306.707.707a1 1 0 0 0 1.414 0l.707-.707L12.7 6.679a1 1 0 0 0-.715-1.711z"/></svg>

Before

Width:  |  Height:  |  Size: 608 B

After

Width:  |  Height:  |  Size: 358 B

View File

@ -0,0 +1 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16"><path fill="context-fill" fill-opacity="context-fill-opacity" d="M14.5.932h-13A1.509 1.509 0 0 0 0 2.435v4.5a8 8 0 0 0 16 0v-4.5A1.508 1.508 0 0 0 14.5.932zm-.5 6a6 6 0 0 1-12 0v-4h12zm-6.7 3.477a1 1 0 0 0 1.422 0l3.343-3.39a1 1 0 1 0-1.423-1.406L8.01 8.283 5.38 5.614a1 1 0 0 0-1.425 1.405zm.711.3z"/></svg>

After

Width:  |  Height:  |  Size: 368 B

View File

@ -0,0 +1,40 @@
# TippyTop in Activity Stream
TippyTop, a collection of icons from the Alexa top sites, provides high quality images for the Top Sites in Activity Stream. The TippyTop manifest is hosted on S3, and then moved to [Remote Settings](https://firefox-source-docs.mozilla.org/services/common/docs/services/RemoteSettings.html) since Firefox 63. In this document, we'll cover how we produce and manage TippyTop manifest for Activity Stream.
## TippyTop manifest production
TippyTop manifest is produced by [tippy-top-sites](https://github.com/mozilla/tippy-top-sites).
```sh
# set up the enviroment, only needed for the first time
$ pip install -r requirements.txt
$ python make_manifest.py --count 2000 > icons.json # Alexa top 2000 sites
```
Because the manifest is hosted remotely, we use another repo [tippytop-service](https://github.com/mozilla-services/tippytop-service) for the version control and deployment. Ask :nanj or :r1cky for permission to access this private repo.
## TippyTop manifest publishing
For each new manifest release, firstly you should tag it in the tippytop-service repo, then publish it as follows:
### For Firefox 62 and below
File a deploy bug with the tagged version at Bugzilla as [Activity Streams: Application Servers](https://bugzilla.mozilla.org/enter_bug.cgi?product=Firefox&component=Activity%20Streams%3A%20Application%20Servers), assign it to our system engineer :jbuck, he will take care of the rest.
### For Firefox 63 and beyond
Activity Stream started using Remote Settings to manage TippyTop manifest since Firefox 63. To be able to publish new manifest, you need to be in the author&reviewer group of Remote Settings. See more details in this [mana page](https://mana.mozilla.org/wiki/pages/viewpage.action?pageId=66655528). You can also ask :nanj or :leplatram to get this set up for you.
To publish the manifest to Remote Settings, go to the tippytop-service repo, and run the script as follows,
```sh
# set up the remote setting, only needed for the first time
$ python3 -m venv .venv
$ source .venv/bin/activate
$ pip install -r requirements.txt
# publish it to prod
$ source .venv/bin/activate
# It will ask you for your LDAP user name and password.
$ ./upload2remotesettings.py prod
```
After uploading it to Remote Setting, you can request for review in the [dashboard](https://settings-writer.prod.mozaws.net/v1/admin/). Note that you will need to log in the Mozilla LDAP VPN for both uploading and accessing Remote Setting's dashboard. Once your request gets approved by the reviewer, the new manifest will be content signed and published to production.
## TippyTop Viwer
You can use this [viwer](https://mozilla.github.io/tippy-top-sites/manifest-viewer/) to load all the icons in the current manifest.

View File

@ -141,7 +141,8 @@ module.exports = function(config) {
path.resolve("test"),
path.resolve("vendor"),
path.resolve("lib/ASRouterTargeting.jsm"),
path.resolve("lib/ASRouterTriggerListeners.jsm")
path.resolve("lib/ASRouterTriggerListeners.jsm"),
path.resolve("lib/OnboardingMessageProvider.jsm")
]
}
]

View File

@ -17,8 +17,7 @@ ChromeUtils.defineModuleGetter(this, "ASRouterTriggerListeners",
const INCOMING_MESSAGE_NAME = "ASRouter:child-to-parent";
const OUTGOING_MESSAGE_NAME = "ASRouter:parent-to-child";
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
const SNIPPETS_ENDPOINT_PREF = "browser.newtabpage.activity-stream.asrouter.snippetsUrl";
const MESSAGE_PROVIDER_PREF = "browser.newtabpage.activity-stream.asrouter.messageProviders";
// List of hosts for endpoints that serve router messages.
// Key is allowed host, value is a name for the endpoint host.
const DEFAULT_WHITELIST_HOSTS = {
@ -27,6 +26,8 @@ const DEFAULT_WHITELIST_HOSTS = {
};
const SNIPPETS_ENDPOINT_WHITELIST = "browser.newtab.activity-stream.asrouter.whitelistHosts";
const LOCAL_MESSAGE_PROVIDERS = {OnboardingMessageProvider};
const MessageLoaderUtils = {
/**
* _localLoader - Loads messages for a local provider (i.e. one that lives in mozilla central)
@ -132,7 +133,7 @@ this.MessageLoaderUtils = MessageLoaderUtils;
* so that it can be more easily unit tested.
*/
class _ASRouter {
constructor(initialState = {}) {
constructor(messageProviderPref = MESSAGE_PROVIDER_PREF, localProviders = LOCAL_MESSAGE_PROVIDERS) {
this.initialized = false;
this.messageChannel = null;
this.dispatchToAS = null;
@ -143,41 +144,52 @@ class _ASRouter {
providers: [],
blockList: [],
impressions: {},
messages: [],
...initialState
messages: []
};
this._triggerHandler = this._triggerHandler.bind(this);
this._messageProviderPref = messageProviderPref;
this._localProviders = localProviders;
this.onMessage = this.onMessage.bind(this);
this._handleTargetingError = this._handleTargetingError.bind(this);
}
_addASRouterPrefListener() {
this.state.providers.forEach(provider => {
if (provider.endpointPref) {
Services.prefs.addObserver(provider.endpointPref, this);
}
});
}
// Update provider endpoint and fetch new messages on pref change
// Update message providers and fetch new messages on pref change
async observe(aSubject, aTopic, aPrefName) {
await this.setState(prevState => {
const providers = [...prevState.providers];
this._updateProviderEndpointUrl(providers.find(p => p.endpointPref === aPrefName));
return {providers};
});
if (aPrefName === this._messageProviderPref) {
this._updateMessageProviders();
}
await this.loadMessagesFromAllProviders();
}
_updateProviderEndpointUrl(provider) {
if (provider && provider.endpointPref) {
provider.url = Services.prefs.getStringPref(provider.endpointPref, "");
// Reset provider update timestamp to force messages refresh
provider.lastUpdated = undefined;
// Fetch and decode the message provider pref JSON, and update the message providers
_updateMessageProviders() {
// If we have added a `preview` provider, hold onto it
const existingPreviewProvider = this.state.providers.find(p => p.id === "preview");
const providers = existingPreviewProvider ? [existingPreviewProvider] : [];
const providersJSON = Services.prefs.getStringPref(this._messageProviderPref, "");
try {
JSON.parse(providersJSON).forEach(provider => providers.push(provider));
} catch (e) {
Cu.reportError("Problem parsing JSON message provider pref for ASRouter");
}
return provider;
providers.forEach(provider => {
if (provider.type === "local" && !provider.messages) {
// Get the messages from the local message provider
const localProvider = this._localProviders[provider.localProvider];
provider.messages = localProvider ? localProvider.getMessages() : [];
}
// Reset provider update timestamp to force message refresh
provider.lastUpdated = undefined;
});
const providerIDs = providers.map(p => p.id);
this.setState(prevState => ({
providers,
// Clear any messages from removed providers
messages: [...prevState.messages.filter(message => providerIDs.includes(message.provider))]
}));
}
get state() {
@ -218,7 +230,7 @@ class _ASRouter {
let newState = {messages: [], providers: []};
for (const provider of this.state.providers) {
if (needsUpdate.includes(provider)) {
const {messages, lastUpdated} = await MessageLoaderUtils.loadMessagesForProvider(this._updateProviderEndpointUrl(provider));
const {messages, lastUpdated} = await MessageLoaderUtils.loadMessagesForProvider(provider);
newState.providers.push({...provider, lastUpdated});
newState.messages = [...newState.messages, ...messages];
} else {
@ -261,7 +273,7 @@ class _ASRouter {
async init(channel, storage, dispatchToAS) {
this.messageChannel = channel;
this.messageChannel.addMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
this._addASRouterPrefListener();
Services.prefs.addObserver(this._messageProviderPref, this);
this._storage = storage;
this.WHITELIST_HOSTS = this._loadSnippetsWhitelistHosts();
this.dispatchToAS = dispatchToAS;
@ -269,6 +281,7 @@ class _ASRouter {
const blockList = await this._storage.get("blockList") || [];
const impressions = await this._storage.get("impressions") || {};
await this.setState({blockList, impressions});
this._updateMessageProviders();
await this.loadMessagesFromAllProviders();
// sets .initialized to true and resolves .waitForInitialized promise
@ -280,11 +293,7 @@ class _ASRouter {
this.messageChannel.removeMessageListener(INCOMING_MESSAGE_NAME, this.onMessage);
this.messageChannel = null;
this.dispatchToAS = null;
this.state.providers.forEach(provider => {
if (provider.endpointPref) {
Services.prefs.removeObserver(provider.endpointPref, this);
}
});
Services.prefs.removeObserver(this._messageProviderPref, this);
// Uninitialise all trigger listeners
for (const listener of ASRouterTriggerListeners.values()) {
listener.uninit();
@ -656,11 +665,6 @@ this._ASRouter = _ASRouter;
* ASRouter - singleton instance of _ASRouter that controls all messages
* in the new tab page.
*/
this.ASRouter = new _ASRouter({
providers: [
{id: "onboarding", type: "local", messages: OnboardingMessageProvider.getMessages()},
{id: "snippets", type: "remote", endpointPref: SNIPPETS_ENDPOINT_PREF, updateCycleInMs: ONE_HOUR_IN_MS * 4}
]
});
this.ASRouter = new _ASRouter();
const EXPORTED_SYMBOLS = ["_ASRouter", "ASRouter", "MessageLoaderUtils"];

View File

@ -42,6 +42,8 @@ const DEFAULT_SITES = new Map([
const GEO_PREF = "browser.search.region";
const SPOCS_GEOS = ["US"];
const ONE_HOUR_IN_MS = 60 * 60 * 1000;
// Determine if spocs should be shown for a geo/locale
function showSpocs({geo}) {
return SPOCS_GEOS.includes(geo);
@ -155,7 +157,11 @@ const PREFS_CONFIG = new Map([
}],
["improvesearch.noDefaultSearchTile", {
title: "Experiment to remove tiles that are the same as the default search",
value: false
value: true
}],
["improvesearch.topSiteSearchShortcuts", {
title: "Experiment to show special top sites that perform keyword searches",
value: true
}],
["asrouterExperimentEnabled", {
title: "Is the message center experiment on?",
@ -165,9 +171,24 @@ const PREFS_CONFIG = new Map([
title: "What cohort is the user in?",
value: 0
}],
["asrouter.snippetsUrl", {
title: "A custom URL for the AS router snippets",
value: "https://activity-stream-icons.services.mozilla.com/v1/messages.json.br"
["asrouter.messageProviders", {
title: "Configuration for ASRouter message providers",
/**
* Each provider must have a unique id and a type of "local" or "remote".
* Local providers must specify the name of an ASRouter message provider.
* Remote providers must specify a `url` and an `updateCycleInMs`.
*/
value: JSON.stringify([{
id: "onboarding",
type: "local",
localProvider: "OnboardingMessageProvider"
}, {
id: "snippets",
type: "remote",
url: "https://activity-stream-icons.services.mozilla.com/v1/messages.json.br",
updateCycleInMs: ONE_HOUR_IN_MS * 4
}])
}]
]);

View File

@ -34,32 +34,24 @@ const ROWS_PREF = "topSitesRows";
// Search experiment stuff
const NO_DEFAULT_SEARCH_TILE_EXP_PREF = "improvesearch.noDefaultSearchTile";
const SEARCH_HOST_FILTERS = [
{hostname: "google", identifierPattern: /^google/},
{hostname: "amazon", identifierPattern: /^amazon/}
const SEARCH_FILTERS = [
"google",
"search.yahoo",
"yahoo",
"bing",
"ask",
"duckduckgo"
];
/**
* isLinkDefaultSearch - does a given hostname match the user's default search engine?
*
* @param {string} hostname a top site hostname, such as "amazon" or "foo"
* @returns {bool}
*/
function isLinkDefaultSearch(hostname) {
for (const searchProvider of SEARCH_HOST_FILTERS) {
if (
hostname === searchProvider.hostname &&
String(Services.search.defaultEngine.identifier).match(searchProvider.identifierPattern)
) {
return true;
}
}
return false;
function getShortURLForCurrentSearch() {
const url = shortURL({url: Services.search.currentEngine.searchForm});
return url;
}
this.TopSitesFeed = class TopSitesFeed {
constructor() {
this._tippyTopProvider = new TippyTopProvider();
this._currentSearchHostname = null;
this.dedupe = new Dedupe(this._dedupeKey);
this.frecentCache = new LinksCache(NewTabUtils.activityStreamLinks,
"getTopSites", CACHED_LINK_PROPS_TO_MIGRATE, (oldOptions, newOptions) =>
@ -76,17 +68,20 @@ this.TopSitesFeed = class TopSitesFeed {
this._storage = this.store.dbStorage.getDbTable("sectionPrefs");
this.refresh({broadcast: true});
Services.obs.addObserver(this, "browser-search-engine-modified");
this._currentSearchHostname = getShortURLForCurrentSearch();
}
uninit() {
PageThumbs.removeExpirationFilter(this);
Services.obs.removeObserver(this, "browser-search-engine-modified");
this._currentSearchHostname = null;
}
observe(subj, topic, data) {
// We should update the current top sites if the search engine has been changed since
// the search engine that gets filtered out of top sites has changed.
if (topic === "browser-search-engine-modified" && data === "engine-default" && this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF]) {
if (topic === "browser-search-engine-modified" && data === "engine-current" && this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF]) {
this._currentSearchHostname = getShortURLForCurrentSearch();
this.refresh({broadcast: true});
}
}
@ -123,8 +118,23 @@ this.TopSitesFeed = class TopSitesFeed {
}, []));
}
/**
* isExperimentOnAndLinkFilteredSearch - is the experiment on and does a given hostname match the user's default search engine?
*
* @param {string} hostname a top site hostname, such as "amazon" or "foo"
* @returns {bool}
*/
isExperimentOnAndLinkFilteredSearch(hostname) {
if (!this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF]) {
return false;
}
if (SEARCH_FILTERS.includes(hostname) || hostname === this._currentSearchHostname) {
return true;
}
return false;
}
async getLinksWithDefaults() {
const isExperimentOn = this.store.getState().Prefs.values[NO_DEFAULT_SEARCH_TILE_EXP_PREF];
const numItems = this.store.getState().Prefs.values[ROWS_PREF] * TOP_SITES_MAX_SITES_PER_ROW;
// Get all frecent sites from history
@ -134,7 +144,7 @@ this.TopSitesFeed = class TopSitesFeed {
}))
.reduce((validLinks, link) => {
const hostname = shortURL(link);
if (!(isExperimentOn && isLinkDefaultSearch(hostname))) {
if (!this.isExperimentOnAndLinkFilteredSearch(hostname)) {
validLinks.push({...link, hostname});
}
return validLinks;
@ -145,7 +155,7 @@ this.TopSitesFeed = class TopSitesFeed {
.filter(link => {
if (NewTabUtils.blockedLinks.isBlocked({url: link.url})) {
return false;
} else if (isExperimentOn && isLinkDefaultSearch(link.hostname)) {
} else if (this.isExperimentOnAndLinkFilteredSearch(link.hostname)) {
return false;
}
return true;

View File

@ -173,3 +173,25 @@ section_menu_action_add_topsite=Med Kakube maloyo
section_menu_action_move_up=Kob Malo
section_menu_action_move_down=Kob Piny
section_menu_action_privacy_notice=Ngec me mung
# LOCALIZATION NOTE (firstrun_*). These strings are displayed only once, on the
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_learn_more_link=Nong ngec mapol ikom Akaunt me Firefox
# LOCALIZATION NOTE (firstrun_form_header and firstrun_form_sub_header):
# firstrun_form_sub_header is a continuation of firstrun_form_header, they are one sentence.
# firstrun_form_header is displayed more boldly as the call to action.
firstrun_form_header=Ket email mamegi
firstrun_email_input_placeholder=Email
firstrun_invalid_input=Email ma tye atir mite
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=Mede anyim nyuto ni i yee {terms} ki {privacy}.
firstrun_terms_of_service=Cik me Tic
firstrun_privacy_notice=Ngec me mung
firstrun_continue_to_login=Mede
firstrun_skip_login=Kal citep man

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=כדי להמשיך אל Firefox Sync.
firstrun_email_input_placeholder=דוא״ל
firstrun_invalid_input=נדרשת כתובת דוא״ל חוקית
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=בחירתך להמשיך בתהליך מהווה את הסכמתך ל{terms} ול{privacy}.

View File

@ -50,8 +50,8 @@ menu_action_archive_pocket=Archivar in Pocket
# "this action" is that it will show where the downloaded file exists on the file system
# for each operating system.
menu_action_show_file_mac_os=Monstrar in Finder
menu_action_show_file_windows=Aperir le plica que lo contine
menu_action_show_file_linux=Aperir le plica que lo contine
menu_action_show_file_windows=Aperir le dossier que lo contine
menu_action_show_file_linux=Aperir le dossier que lo contine
menu_action_show_file_default=Monstrar le file
menu_action_open_file=Aperir le file
@ -181,7 +181,7 @@ section_menu_action_privacy_notice=Notification de confidentialitate
# firstrun of the browser, they give an introduction to Firefox and Sync.
firstrun_title=Porta Firefox con te
firstrun_content=Tene tu marcapaginas, chronologia, contrasignos e altere configurationes sur tote tu apparatos.
firstrun_learn_more_link=Apprende plus re Firefox Accounts
firstrun_learn_more_link=Saper plus super Firefox Accounts
# 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.

View File

@ -191,6 +191,8 @@ firstrun_form_sub_header=해서 Firefox Sync 사용
firstrun_email_input_placeholder=이메일
firstrun_invalid_input=유효한 이메일 필요함
# LOCALIZATION NOTE (firstrun_extra_legal_links): {terms} is equal to firstrun_terms_of_service, and
# {privacy} is equal to firstrun_privacy_notice. {terms} and {privacy} are clickable links.
firstrun_extra_legal_links=진행하면 {terms}과 {privacy}에 동의하게 됩니다.

View File

@ -148,6 +148,7 @@ manual_migration_import_button=ابھی درآمد کری
# LOCALIZATION NOTE (section_menu_action_*). These strings are displayed in the section
# context menu and are meant as a call to action for the given section.
section_menu_action_manage_webext=توسیع بندرست کریں
section_menu_action_add_topsite=بہترین سائٹ شامل کریں
section_menu_action_move_up=اوپر کریں
section_menu_action_move_down=نیچے کریں
section_menu_action_privacy_notice=رازداری کا نوٹس

View File

@ -92,14 +92,14 @@ window.gActivityStreamStrings = {
"section_menu_action_privacy_notice": "Ngec me mung",
"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_learn_more_link": "Nong ngec mapol ikom Akaunt me Firefox",
"firstrun_form_header": "Ket email mamegi",
"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"
"firstrun_invalid_input": "Email ma tye atir mite",
"firstrun_extra_legal_links": "Mede anyim nyuto ni i yee {terms} ki {privacy}.",
"firstrun_terms_of_service": "Cik me Tic",
"firstrun_privacy_notice": "Ngec me mung",
"firstrun_continue_to_login": "Mede",
"firstrun_skip_login": "Kal citep man"
};

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "נא להקליד את כתובת הדוא״ל שלך",
"firstrun_form_sub_header": "כדי להמשיך אל Firefox Sync.",
"firstrun_email_input_placeholder": "דוא״ל",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "נדרשת כתובת דוא״ל חוקית",
"firstrun_extra_legal_links": "בחירתך להמשיך בתהליך מהווה את הסכמתך ל{terms} ול{privacy}.",
"firstrun_terms_of_service": "תנאי השירות",
"firstrun_privacy_notice": "הצהרת הפרטיות",

View File

@ -25,8 +25,8 @@ window.gActivityStreamStrings = {
"menu_action_delete_pocket": "Delite ex Pocket",
"menu_action_archive_pocket": "Archivar in Pocket",
"menu_action_show_file_mac_os": "Monstrar in Finder",
"menu_action_show_file_windows": "Aperir le plica que lo contine",
"menu_action_show_file_linux": "Aperir le plica que lo contine",
"menu_action_show_file_windows": "Aperir le dossier que lo contine",
"menu_action_show_file_linux": "Aperir le dossier que lo contine",
"menu_action_show_file_default": "Monstrar le file",
"menu_action_open_file": "Aperir le file",
"menu_action_copy_download_link": "Copiar le ligamine de discargamento",
@ -92,7 +92,7 @@ window.gActivityStreamStrings = {
"section_menu_action_privacy_notice": "Notification de confidentialitate",
"firstrun_title": "Porta Firefox con te",
"firstrun_content": "Tene tu marcapaginas, chronologia, contrasignos e altere configurationes sur tote tu apparatos.",
"firstrun_learn_more_link": "Apprende plus re Firefox Accounts",
"firstrun_learn_more_link": "Saper plus super Firefox Accounts",
"firstrun_form_header": "Insere tu email",
"firstrun_form_sub_header": "pro continuar con Firefox Sync.",
"firstrun_email_input_placeholder": "Email",

View File

@ -96,7 +96,7 @@ window.gActivityStreamStrings = {
"firstrun_form_header": "이메일을 입력",
"firstrun_form_sub_header": "해서 Firefox Sync 사용",
"firstrun_email_input_placeholder": "이메일",
"firstrun_invalid_input": "Valid email required",
"firstrun_invalid_input": "유효한 이메일 필요함",
"firstrun_extra_legal_links": "진행하면 {terms}과 {privacy}에 동의하게 됩니다.",
"firstrun_terms_of_service": "이용 약관",
"firstrun_privacy_notice": "개인 정보 보호 정책",

View File

@ -86,7 +86,7 @@ window.gActivityStreamStrings = {
"section_menu_action_expand_section": "Expand Section",
"section_menu_action_manage_section": "Manage Section",
"section_menu_action_manage_webext": "توسیع بندرست کریں",
"section_menu_action_add_topsite": "Add Top Site",
"section_menu_action_add_topsite": "بہترین سائٹ شامل کریں",
"section_menu_action_move_up": "اوپر کریں",
"section_menu_action_move_down": "نیچے کریں",
"section_menu_action_privacy_notice": "رازداری کا نوٹس",

View File

@ -3,6 +3,7 @@ import {
CHILD_TO_PARENT_MESSAGE_NAME,
FAKE_LOCAL_MESSAGES,
FAKE_LOCAL_PROVIDER,
FAKE_LOCAL_PROVIDERS,
FAKE_REMOTE_MESSAGES,
FAKE_REMOTE_PROVIDER,
FakeRemotePageManager,
@ -10,6 +11,7 @@ import {
} from "./constants";
import {ASRouterTriggerListeners} from "lib/ASRouterTriggerListeners.jsm";
const MESSAGE_PROVIDER_PREF_NAME = "browser.newtabpage.activity-stream.asrouter.messageProviders";
const FAKE_PROVIDERS = [FAKE_LOCAL_PROVIDER, FAKE_REMOTE_PROVIDER];
const ALL_MESSAGE_IDS = [...FAKE_LOCAL_MESSAGES, ...FAKE_REMOTE_MESSAGES].map(message => message.id);
const FAKE_BUNDLE = [FAKE_LOCAL_MESSAGES[1], FAKE_LOCAL_MESSAGES[2]];
@ -43,10 +45,17 @@ describe("ASRouter", () => {
};
}
function setMessageProviderPref(value, prefName = MESSAGE_PROVIDER_PREF_NAME) {
getStringPrefStub
.withArgs(prefName, "")
.returns(JSON.stringify(value));
}
async function createRouterAndInit(providers = FAKE_PROVIDERS) {
setMessageProviderPref(providers);
channel = new FakeRemotePageManager();
Router = new _ASRouter({providers});
dispatchStub = sandbox.stub();
Router = new _ASRouter(MESSAGE_PROVIDER_PREF_NAME, FAKE_LOCAL_PROVIDERS);
await Router.init(channel, createFakeStorage(), dispatchStub);
}
@ -59,7 +68,6 @@ describe("ASRouter", () => {
.withArgs("http://fake.com/endpoint")
.resolves({ok: true, status: 200, json: () => Promise.resolve({messages: FAKE_REMOTE_MESSAGES})});
getStringPrefStub = sandbox.stub(global.Services.prefs, "getStringPref");
getStringPrefStub.returns("http://fake.com/endpoint");
addObserverStub = sandbox.stub(global.Services.prefs, "addObserver");
await createRouterAndInit();
@ -82,9 +90,9 @@ describe("ASRouter", () => {
const [, listenerAdded] = channel.addMessageListener.firstCall.args;
assert.isFunction(listenerAdded);
});
it("should add an observer for each provider with a defined endpointPref", () => {
it("should add an observer for the messageProviderPref", () => {
assert.calledOnce(addObserverStub);
assert.calledWith(addObserverStub, "remotePref");
assert.calledWith(addObserverStub, MESSAGE_PROVIDER_PREF_NAME);
});
it("should set state.blockList to the block list in persistent storage", async () => {
blockList = ["foo"];
@ -105,7 +113,7 @@ describe("ASRouter", () => {
assert.deepEqual(Router.state.impressions, impressions);
});
it("should await .loadMessagesFromAllProviders() and add messages from providers to state.messages", async () => {
Router = new _ASRouter({providers: FAKE_PROVIDERS});
Router = new _ASRouter(MESSAGE_PROVIDER_PREF_NAME, FAKE_LOCAL_PROVIDERS);
const loadMessagesSpy = sandbox.spy(Router, "loadMessagesFromAllProviders");
await Router.init(channel, createFakeStorage(), dispatchStub);
@ -114,20 +122,21 @@ describe("ASRouter", () => {
assert.isArray(Router.state.messages);
assert.lengthOf(Router.state.messages, FAKE_LOCAL_MESSAGES.length + FAKE_REMOTE_MESSAGES.length);
});
it("should call loadMessagesFromAllProviders on pref endpoint change", async () => {
it("should call loadMessagesFromAllProviders on pref change", async () => {
sandbox.spy(Router, "loadMessagesFromAllProviders");
await Router.observe();
assert.calledOnce(Router.loadMessagesFromAllProviders);
});
it("should update provider url on pref change", async () => {
getStringPrefStub.withArgs("remotePref").returns("baz.com");
it("should update provider on pref change", async () => {
const modifiedRemoteProvider = Object.assign({}, FAKE_REMOTE_PROVIDER, {url: "baz.com"});
setMessageProviderPref([FAKE_LOCAL_PROVIDER, modifiedRemoteProvider]);
const {length} = Router.state.providers;
await Router.observe("", "", "remotePref");
await Router.observe("", "", MESSAGE_PROVIDER_PREF_NAME);
const provider = Router.state.providers.find(p => p.url === "baz.com");
assert.lengthOf(Router.state.providers, length);
assert.isDefined(provider);
});
@ -160,17 +169,6 @@ describe("ASRouter", () => {
}
}
it("should load provider endpoint based on pref", async () => {
getStringPrefStub.reset();
getStringPrefStub.returns("example.com");
await createRouterAndInit();
// Get snippets endpoint url, get the whitelisted hosts for endpoints
assert.calledTwice(getStringPrefStub);
assert.calledWithExactly(getStringPrefStub, "remotePref", "");
assert.calledWithExactly(getStringPrefStub, "browser.newtab.activity-stream.asrouter.whitelistHosts", "");
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 () => {
await createRouterAndInit([
{id: "remotey", type: "remote", url: "http://fake.com/endpoint", updateCycleInMs: 300}

View File

@ -9,12 +9,13 @@ export const FAKE_LOCAL_MESSAGES = [
{id: "bar", template: "fancy_template", content: {title: "Foo", body: "Foo123"}},
{id: "baz", content: {title: "Foo", body: "Foo123"}}
];
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", messages: FAKE_LOCAL_MESSAGES};
export const FAKE_LOCAL_PROVIDER = {id: "onboarding", type: "local", localProvider: "FAKE_LOCAL_PROVIDER"};
export const FAKE_LOCAL_PROVIDERS = {FAKE_LOCAL_PROVIDER: {getMessages: () => FAKE_LOCAL_MESSAGES}};
export const FAKE_REMOTE_MESSAGES = [
{id: "qux", template: "simple_template", content: {title: "Qux", body: "hello world"}}
];
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint", endpointPref: "remotePref"};
export const FAKE_REMOTE_PROVIDER = {id: "remotey", type: "remote", url: "http://fake.com/endpoint"};
// Stubs methods on RemotePageManager
export class FakeRemotePageManager {

View File

@ -1088,35 +1088,60 @@ describe("Top Sites Feed", () => {
const NO_DEFAULT_SEARCH_TILE_PREF = "improvesearch.noDefaultSearchTile";
let cachedDefaultSearch;
beforeEach(() => {
cachedDefaultSearch = global.Services.search.defaultEngine;
global.Services.search.defaultEngine = {identifier: "google"};
cachedDefaultSearch = global.Services.search.currentEngine;
global.Services.search.currentEngine = {identifier: "google", searchForm: "google.com"};
feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = true;
});
afterEach(() => {
global.Services.search.defaultEngine = cachedDefaultSearch;
global.Services.search.currentEngine = cachedDefaultSearch;
});
it("should not filter out google from the query results if the experiment pref is off", async () => {
links = [{url: "google.com"}, {url: "foo.com"}];
it("should filter out alexa top 5 search from the default sites", async () => {
const TOP_5_TEST = [
"google.com",
"search.yahoo.com",
"yahoo.com",
"bing.com",
"ask.com",
"duckduckgo.com"
];
links = [{url: "amazon.com"}, ...TOP_5_TEST.map(url => ({url}))];
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "amazon.com");
TOP_5_TEST.forEach(url => assert.notInclude(urlsReturned, url));
});
it("should not filter out alexa, default search from the query results if the experiment pref is off", async () => {
links = [{url: "google.com"}, {url: "foo.com"}, {url: "duckduckgo"}];
feed.store.state.Prefs.values[NO_DEFAULT_SEARCH_TILE_PREF] = false;
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "google.com");
});
it("should filter out google from the default sites if it matches the current default search", async () => {
it("should filter out the current default search from the default sites", async () => {
feed._currentSearchHostname = "amazon";
feed.onAction({type: at.PREFS_INITIAL_VALUES, data: {"default.sites": "google.com,amazon.com"}});
links = [{url: "foo.com"}];
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "amazon.com");
assert.notInclude(urlsReturned, "google.com");
assert.notInclude(urlsReturned, "amazon.com");
});
it("should not filter out google from pinned sites even if it matches the current default search", async () => {
it("should not filter out current default search from pinned sites even if it matches the current default search", async () => {
links = [{url: "foo.com"}];
fakeNewTabUtils.pinnedLinks.links = [{url: "google.com"}];
const urlsReturned = (await feed.getLinksWithDefaults()).map(link => link.url);
assert.include(urlsReturned, "google.com");
});
it("should call refresh when the the default search engine has been set", () => {
it("should set ._currentSearchHostname to the current engine hostname on init", async () => {
global.Services.search.currentEngine = {identifier: "ddg", searchForm: "duckduckgo.com"};
sandbox.stub(feed, "refresh");
await feed.init();
assert.equal(feed._currentSearchHostname, "duckduckgo");
});
it("should call refresh and set ._currentSearchHostname to the new engine hostname when the the default search engine has been set", () => {
sinon.stub(feed, "refresh");
feed.observe(null, "browser-search-engine-modified", "engine-default");
global.Services.search.currentEngine = {identifier: "ddg", searchForm: "duckduckgo.com"};
feed.observe(null, "browser-search-engine-modified", "engine-current");
assert.equal(feed._currentSearchHostname, "duckduckgo");
assert.calledOnce(feed.refresh);
});
it("should call refresh when the experiment pref has changed", () => {
sinon.stub(feed, "refresh");

View File

@ -173,7 +173,8 @@ const TEST_GLOBAL = {
search: {
init(cb) { cb(); },
getVisibleEngines: () => [{identifier: "google"}, {identifier: "bing"}],
defaultEngine: {identifier: "google"}
defaultEngine: {identifier: "google"},
currentEngine: {identifier: "google", searchForm: "https://www.google.com/search?q=&ie=utf-8&oe=utf-8&client=firefox-b"}
},
scriptSecurityManager: {
createNullPrincipal() {},