Source code

Revision control

Copy as Markdown

Other Tools

/* 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/. */
import {
actionCreators as ac,
actionTypes as at,
// We use importESModule here instead of static import so that
// the Karma test environment won't choke on this module. This
// is because the Karma test environment already stubs out
// AppConstants, and overrides importESModule to be a no-op (which
// can't be done for a static import statement).
// eslint-disable-next-line mozilla/use-static-import
const { AppConstants } = ChromeUtils.importESModule(
"resource://gre/modules/AppConstants.sys.mjs"
);
const lazy = {};
ChromeUtils.defineESModuleGetters(lazy, {
NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs",
PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs",
Region: "resource://gre/modules/Region.sys.mjs",
});
export class PrefsFeed {
constructor(prefMap) {
this._prefMap = prefMap;
this._prefs = new Prefs();
this.onExperimentUpdated = this.onExperimentUpdated.bind(this);
this.onTrainhopExperimentUpdated =
this.onTrainhopExperimentUpdated.bind(this);
this.onPocketExperimentUpdated = this.onPocketExperimentUpdated.bind(this);
this.onSmartShortcutsExperimentUpdated =
this.onSmartShortcutsExperimentUpdated.bind(this);
this.onWidgetsUpdated = this.onWidgetsUpdated.bind(this);
this.onOhttpImagesUpdated = this.onOhttpImagesUpdated.bind(this);
this.onInferredPersonalizationExperimentUpdated =
this.onInferredPersonalizationExperimentUpdated.bind(this);
}
onPrefChanged(name, value) {
const prefItem = this._prefMap.get(name);
if (prefItem) {
let action = "BroadcastToContent";
if (prefItem.skipBroadcast) {
action = "OnlyToMain";
if (prefItem.alsoToPreloaded) {
action = "AlsoToPreloaded";
}
}
this.store.dispatch(
ac[action]({
type: at.PREF_CHANGED,
data: { name, value },
})
);
}
}
_setStringPref(values, key, defaultValue) {
this._setPref(values, key, defaultValue, Services.prefs.getStringPref);
}
_setBoolPref(values, key, defaultValue) {
this._setPref(values, key, defaultValue, Services.prefs.getBoolPref);
}
_setIntPref(values, key, defaultValue) {
this._setPref(values, key, defaultValue, Services.prefs.getIntPref);
}
_setPref(values, key, defaultValue, getPrefFunction) {
let value = getPrefFunction(
`browser.newtabpage.activity-stream.${key}`,
defaultValue
);
values[key] = value;
this._prefMap.set(key, { value });
}
/**
* Handler for when experiment data updates.
*/
onExperimentUpdated() {
const value = lazy.NimbusFeatures.newtab.getAllVariables() || {};
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "featureConfig",
value,
},
})
);
}
/**
* Handler for when experiment data updates.
*/
onTrainhopExperimentUpdated() {
const allEnrollments =
lazy.NimbusFeatures.newtabTrainhop.getAllEnrollments() || [];
// Combine all trainhop experiments keyed by type.
// Rules for duplicates:
// - Experiments take precedence over rollouts (this is expected).
// - If multiple experiments or multiple rollouts exist for the same type,
// only the first is kept. This is nondeterministic and considered an error;
// those experiments/rollouts should be relaunched.
const value = {};
allEnrollments.reduce((accumulator, currentValue) => {
if (currentValue?.value?.type) {
if (
!accumulator[currentValue.value.type] ||
(accumulator[currentValue.value.type].meta.isRollout &&
!currentValue.meta.isRollout)
) {
accumulator[currentValue.value.type] = currentValue;
// Shorten the data chain.
value[currentValue.value.type] = currentValue.value.payload;
}
}
return accumulator;
}, {});
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "trainhopConfig",
value,
},
})
);
}
/**
* Handler for Pocket specific experiment data updates.
*/
onPocketExperimentUpdated(event, reason) {
const value = lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {};
// Loaded experiments are set up inside init()
if (
reason !== "feature-experiment-loaded" &&
reason !== "feature-rollout-loaded"
) {
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "pocketConfig",
value,
},
})
);
}
}
/**
* Handler for when smart shortcuts experiment data updates.
*/
onSmartShortcutsExperimentUpdated() {
const value =
lazy.NimbusFeatures.newtabSmartShortcuts.getAllVariables() || {};
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "smartShortcutsConfig",
value,
},
})
);
}
/**
* Handler for when inferred personalization experiment config values update.
*/
onInferredPersonalizationExperimentUpdated() {
const value =
lazy.NimbusFeatures.newtabInferredPersonalization.getAllVariables() || {};
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "inferredPersonalizationConfig",
value,
},
})
);
}
/**
* Handler for when widget experiment data updates.
*/
onWidgetsUpdated() {
const value = lazy.NimbusFeatures.newtabWidgets.getAllVariables() || {};
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "widgetsConfig",
value,
},
})
);
}
/**
* Handler for when OHTTP images experiment data updates.
*/
onOhttpImagesUpdated() {
// newtabOhttpImages was introduced in 143.
const value =
lazy.NimbusFeatures.newtabOhttpImages?.getAllVariables() || {};
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: {
name: "ohttpImagesConfig",
value,
},
})
);
}
init() {
this._prefs.observeBranch(this);
lazy.NimbusFeatures.newtab.onUpdate(this.onExperimentUpdated);
lazy.NimbusFeatures.newtabTrainhop.onUpdate(
this.onTrainhopExperimentUpdated
);
lazy.NimbusFeatures.pocketNewtab.onUpdate(this.onPocketExperimentUpdated);
lazy.NimbusFeatures.newtabSmartShortcuts.onUpdate(
this.onSmartShortcutsExperimentUpdated
);
lazy.NimbusFeatures.newtabInferredPersonalization.onUpdate(
this.onInferredPersonalizationExperimentUpdated
);
lazy.NimbusFeatures.newtabWidgets.onUpdate(this.onWidgetsUpdated);
// newtabOhttpImages was introduced in 143.
if (lazy.NimbusFeatures.newtabOhttpImages) {
lazy.NimbusFeatures.newtabOhttpImages.onUpdate(this.onOhttpImagesUpdated);
}
// Get the initial value of each activity stream pref
const values = {};
for (const name of this._prefMap.keys()) {
values[name] = this._prefs.get(name);
}
// These are not prefs, but are needed to determine stuff in content that can only be
// computed in main process
values.isPrivateBrowsingEnabled = lazy.PrivateBrowsingUtils.enabled;
values.platform = AppConstants.platform;
// Save the geo pref if we have it
if (lazy.Region.home) {
values.region = lazy.Region.home;
this.geo = values.region;
} else if (this.geo !== "") {
// Watch for geo changes and use a dummy value for now
Services.obs.addObserver(this, lazy.Region.REGION_TOPIC);
this.geo = "";
}
// Get the firefox accounts url for links and to send firstrun metrics to.
values.fxa_endpoint = Services.prefs.getStringPref(
"browser.newtabpage.activity-stream.fxaccounts.endpoint",
);
// Get the firefox update channel with values as default, nightly, beta or release
values.appUpdateChannel = Services.prefs.getStringPref(
"app.update.channel",
""
);
// Read the pref for search shortcuts top sites experiment from firefox.js and store it
// in our internal list of prefs to watch
let searchTopSiteExperimentPrefValue = Services.prefs.getBoolPref(
"browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts"
);
values["improvesearch.topSiteSearchShortcuts"] =
searchTopSiteExperimentPrefValue;
this._prefMap.set("improvesearch.topSiteSearchShortcuts", {
value: searchTopSiteExperimentPrefValue,
});
values.mayHaveSponsoredTopSites = Services.prefs.getBoolPref(
"browser.topsites.useRemoteSetting"
);
// Read the pref for search hand-off from firefox.js and store it
// in our internal list of prefs to watch
let handoffToAwesomebarPrefValue = Services.prefs.getBoolPref(
"browser.newtabpage.activity-stream.improvesearch.handoffToAwesomebar"
);
values["improvesearch.handoffToAwesomebar"] = handoffToAwesomebarPrefValue;
this._prefMap.set("improvesearch.handoffToAwesomebar", {
value: handoffToAwesomebarPrefValue,
});
// Add experiment values and default values
values.featureConfig = lazy.NimbusFeatures.newtab.getAllVariables() || {};
values.pocketConfig =
lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {};
values.smartShortcutsConfig =
lazy.NimbusFeatures.newtabSmartShortcuts.getAllVariables() || {};
values.widgetsConfig =
lazy.NimbusFeatures.newtabWidgets.getAllVariables() || {};
this._setBoolPref(values, "logowordmark.alwaysVisible", false);
this._setBoolPref(values, "feeds.section.topstories", false);
this._setBoolPref(values, "discoverystream.enabled", false);
this._setBoolPref(values, "discoverystream.hardcoded-basic-layout", false);
this._setBoolPref(values, "discoverystream.personalization.enabled", false);
this._setBoolPref(
values,
"discoverystream.personalization.override",
false
);
this._setStringPref(
values,
"discoverystream.personalization.modelKeys",
""
);
this._setStringPref(values, "discoverystream.spocs-endpoint", "");
this._setStringPref(values, "discoverystream.spocs-endpoint-query", "");
this._setStringPref(values, "newNewtabExperience.colors", "");
// Set the initial state of all prefs in redux
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREFS_INITIAL_VALUES,
data: values,
meta: {
isStartup: true,
},
})
);
}
uninit() {
this.removeListeners();
}
removeListeners() {
this._prefs.ignoreBranch(this);
lazy.NimbusFeatures.newtab.offUpdate(this.onExperimentUpdated);
lazy.NimbusFeatures.newtabTrainhop.offUpdate(
this.onTrainhopExperimentUpdated
);
lazy.NimbusFeatures.pocketNewtab.offUpdate(this.onPocketExperimentUpdated);
lazy.NimbusFeatures.newtabSmartShortcuts.offUpdate(
this.onSmartShortcutsExperimentUpdated
);
lazy.NimbusFeatures.newtabInferredPersonalization.offUpdate(
this.onInferredPersonalizationExperimentUpdated
);
lazy.NimbusFeatures.newtabWidgets.offUpdate(this.onWidgetsUpdated);
// newtabOhttpImages was introduced in 143.
if (lazy.NimbusFeatures.newtabOhttpImages) {
lazy.NimbusFeatures.newtabOhttpImages.offUpdate(
this.onOhttpImagesUpdated
);
}
if (this.geo === "") {
Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC);
}
}
observe(subject, topic) {
switch (topic) {
case lazy.Region.REGION_TOPIC:
this.store.dispatch(
ac.BroadcastToContent({
type: at.PREF_CHANGED,
data: { name: "region", value: lazy.Region.home },
})
);
break;
}
}
onAction(action) {
switch (action.type) {
case at.INIT:
this.init();
break;
case at.UNINIT:
this.uninit();
break;
case at.CLEAR_PREF:
Services.prefs.clearUserPref(this._prefs._branchStr + action.data.name);
break;
case at.SET_PREF:
this._prefs.set(action.data.name, action.data.value);
break;
}
}
}