зеркало из https://github.com/mozilla/gecko-dev.git
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:
Родитель
21e6277fe3
Коммит
613dc273cb
|
@ -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}}
|
||||
|
|
|
@ -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');
|
||||
}
|
||||
|
|
|
@ -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 {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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 {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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 {
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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 } }
|
||||
|
|
Различия файлов скрыты, потому что одна или несколько строк слишком длинны
|
@ -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>
|
До Ширина: | Высота: | Размер: 608 B После Ширина: | Высота: | Размер: 358 B |
|
@ -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>
|
После Ширина: | Высота: | Размер: 368 B |
|
@ -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.
|
|
@ -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")
|
||||
]
|
||||
}
|
||||
]
|
||||
|
|
|
@ -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"];
|
||||
|
|
|
@ -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
|
||||
}])
|
||||
}]
|
||||
]);
|
||||
|
||||
|
|
|
@ -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;
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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}.
|
||||
|
|
|
@ -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.
|
||||
|
|
|
@ -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}에 동의하게 됩니다.
|
||||
|
|
|
@ -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=رازداری کا نوٹس
|
||||
|
|
|
@ -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"
|
||||
};
|
||||
|
|
|
@ -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": "הצהרת הפרטיות",
|
||||
|
|
|
@ -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",
|
||||
|
|
|
@ -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": "개인 정보 보호 정책",
|
||||
|
|
|
@ -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": "رازداری کا نوٹس",
|
||||
|
|
|
@ -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}
|
||||
|
|
|
@ -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 {
|
||||
|
|
|
@ -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");
|
||||
|
|
|
@ -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() {},
|
||||
|
|
Загрузка…
Ссылка в новой задаче