PrefsFeed.sys.mjs (12969B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 import { 6 actionCreators as ac, 7 actionTypes as at, 8 } from "resource://newtab/common/Actions.mjs"; 9 import { Prefs } from "resource://newtab/lib/ActivityStreamPrefs.sys.mjs"; 10 11 // We use importESModule here instead of static import so that 12 // the Karma test environment won't choke on this module. This 13 // is because the Karma test environment already stubs out 14 // AppConstants, and overrides importESModule to be a no-op (which 15 // can't be done for a static import statement). 16 17 // eslint-disable-next-line mozilla/use-static-import 18 const { AppConstants } = ChromeUtils.importESModule( 19 "resource://gre/modules/AppConstants.sys.mjs" 20 ); 21 22 const lazy = {}; 23 24 ChromeUtils.defineESModuleGetters(lazy, { 25 NimbusFeatures: "resource://nimbus/ExperimentAPI.sys.mjs", 26 PrivateBrowsingUtils: "resource://gre/modules/PrivateBrowsingUtils.sys.mjs", 27 Region: "resource://gre/modules/Region.sys.mjs", 28 }); 29 30 export class PrefsFeed { 31 constructor(prefMap) { 32 this._prefMap = prefMap; 33 this._prefs = new Prefs(); 34 this.onExperimentUpdated = this.onExperimentUpdated.bind(this); 35 this.onTrainhopExperimentUpdated = 36 this.onTrainhopExperimentUpdated.bind(this); 37 this.onPocketExperimentUpdated = this.onPocketExperimentUpdated.bind(this); 38 this.onSmartShortcutsExperimentUpdated = 39 this.onSmartShortcutsExperimentUpdated.bind(this); 40 this.onWidgetsUpdated = this.onWidgetsUpdated.bind(this); 41 this.onOhttpImagesUpdated = this.onOhttpImagesUpdated.bind(this); 42 this.onInferredPersonalizationExperimentUpdated = 43 this.onInferredPersonalizationExperimentUpdated.bind(this); 44 } 45 46 onPrefChanged(name, value) { 47 const prefItem = this._prefMap.get(name); 48 if (prefItem) { 49 let action = "BroadcastToContent"; 50 if (prefItem.skipBroadcast) { 51 action = "OnlyToMain"; 52 if (prefItem.alsoToPreloaded) { 53 action = "AlsoToPreloaded"; 54 } 55 } 56 57 this.store.dispatch( 58 ac[action]({ 59 type: at.PREF_CHANGED, 60 data: { name, value }, 61 }) 62 ); 63 } 64 } 65 66 _setStringPref(values, key, defaultValue) { 67 this._setPref(values, key, defaultValue, Services.prefs.getStringPref); 68 } 69 70 _setBoolPref(values, key, defaultValue) { 71 this._setPref(values, key, defaultValue, Services.prefs.getBoolPref); 72 } 73 74 _setIntPref(values, key, defaultValue) { 75 this._setPref(values, key, defaultValue, Services.prefs.getIntPref); 76 } 77 78 _setPref(values, key, defaultValue, getPrefFunction) { 79 let value = getPrefFunction( 80 `browser.newtabpage.activity-stream.${key}`, 81 defaultValue 82 ); 83 values[key] = value; 84 this._prefMap.set(key, { value }); 85 } 86 87 /** 88 * Handler for when experiment data updates. 89 */ 90 onExperimentUpdated() { 91 const value = lazy.NimbusFeatures.newtab.getAllVariables() || {}; 92 this.store.dispatch( 93 ac.BroadcastToContent({ 94 type: at.PREF_CHANGED, 95 data: { 96 name: "featureConfig", 97 value, 98 }, 99 }) 100 ); 101 } 102 103 /** 104 * Computes the trainhop config by processing all enrollments. 105 * Supports two formats: 106 * - Single payload: { type: "feature", payload: { "enabled": true, ... }} 107 * - Multi-payload: { type: "multi-payload", payload: [{ type: "feature", payload: { "enabled": true, ... }}] } 108 * Both formats output the same structure: { "feature": { "enabled": true, ... }} 109 */ 110 _getTrainhopConfig() { 111 const allEnrollments = 112 lazy.NimbusFeatures.newtabTrainhop.getAllEnrollments() || []; 113 114 let enrollmentsToProcess = []; 115 116 allEnrollments.forEach(enrollment => { 117 if ( 118 enrollment?.value?.type === "multi-payload" && 119 Array.isArray(enrollment?.value?.payload) 120 ) { 121 enrollment.value.payload.forEach(item => { 122 if (item?.type && item?.payload) { 123 enrollmentsToProcess.push({ 124 value: { 125 type: item.type, 126 payload: item.payload, 127 }, 128 meta: enrollment.meta, 129 }); 130 } 131 }); 132 } else if (enrollment?.value?.type) { 133 enrollmentsToProcess.push(enrollment); 134 } 135 }); 136 137 const valueObj = {}; 138 enrollmentsToProcess.reduce((accumulator, currentValue) => { 139 if (currentValue?.value?.type) { 140 if ( 141 !accumulator[currentValue.value.type] || 142 (accumulator[currentValue.value.type].meta.isRollout && 143 !currentValue.meta.isRollout) 144 ) { 145 accumulator[currentValue.value.type] = currentValue; 146 valueObj[currentValue.value.type] = currentValue.value.payload; 147 } 148 } 149 return accumulator; 150 }, {}); 151 152 return valueObj; 153 } 154 155 /** 156 * Handler for when experiment data updates. 157 */ 158 onTrainhopExperimentUpdated() { 159 const valueObj = this._getTrainhopConfig(); 160 161 this.store.dispatch( 162 ac.BroadcastToContent({ 163 type: at.PREF_CHANGED, 164 data: { 165 name: "trainhopConfig", 166 value: valueObj, 167 }, 168 }) 169 ); 170 } 171 172 /** 173 * Handler for Pocket specific experiment data updates. 174 */ 175 onPocketExperimentUpdated(event, reason) { 176 const value = lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {}; 177 // Loaded experiments are set up inside init() 178 if ( 179 reason !== "feature-experiment-loaded" && 180 reason !== "feature-rollout-loaded" 181 ) { 182 this.store.dispatch( 183 ac.BroadcastToContent({ 184 type: at.PREF_CHANGED, 185 data: { 186 name: "pocketConfig", 187 value, 188 }, 189 }) 190 ); 191 } 192 } 193 194 /** 195 * Handler for when smart shortcuts experiment data updates. 196 */ 197 onSmartShortcutsExperimentUpdated() { 198 const value = 199 lazy.NimbusFeatures.newtabSmartShortcuts.getAllVariables() || {}; 200 this.store.dispatch( 201 ac.BroadcastToContent({ 202 type: at.PREF_CHANGED, 203 data: { 204 name: "smartShortcutsConfig", 205 value, 206 }, 207 }) 208 ); 209 } 210 211 /** 212 * Handler for when inferred personalization experiment config values update. 213 */ 214 onInferredPersonalizationExperimentUpdated() { 215 const value = 216 lazy.NimbusFeatures.newtabInferredPersonalization.getAllVariables() || {}; 217 this.store.dispatch( 218 ac.BroadcastToContent({ 219 type: at.PREF_CHANGED, 220 data: { 221 name: "inferredPersonalizationConfig", 222 value, 223 }, 224 }) 225 ); 226 } 227 228 /** 229 * Handler for when widget experiment data updates. 230 */ 231 onWidgetsUpdated() { 232 const value = lazy.NimbusFeatures.newtabWidgets.getAllVariables() || {}; 233 this.store.dispatch( 234 ac.BroadcastToContent({ 235 type: at.PREF_CHANGED, 236 data: { 237 name: "widgetsConfig", 238 value, 239 }, 240 }) 241 ); 242 } 243 244 /** 245 * Handler for when OHTTP images experiment data updates. 246 */ 247 onOhttpImagesUpdated() { 248 const value = lazy.NimbusFeatures.newtabOhttpImages.getAllVariables() || {}; 249 this.store.dispatch( 250 ac.BroadcastToContent({ 251 type: at.PREF_CHANGED, 252 data: { 253 name: "ohttpImagesConfig", 254 value, 255 }, 256 }) 257 ); 258 } 259 260 init() { 261 this._prefs.observeBranch(this); 262 lazy.NimbusFeatures.newtab.onUpdate(this.onExperimentUpdated); 263 lazy.NimbusFeatures.newtabTrainhop.onUpdate( 264 this.onTrainhopExperimentUpdated 265 ); 266 lazy.NimbusFeatures.pocketNewtab.onUpdate(this.onPocketExperimentUpdated); 267 lazy.NimbusFeatures.newtabSmartShortcuts.onUpdate( 268 this.onSmartShortcutsExperimentUpdated 269 ); 270 lazy.NimbusFeatures.newtabInferredPersonalization.onUpdate( 271 this.onInferredPersonalizationExperimentUpdated 272 ); 273 lazy.NimbusFeatures.newtabWidgets.onUpdate(this.onWidgetsUpdated); 274 lazy.NimbusFeatures.newtabOhttpImages.onUpdate(this.onOhttpImagesUpdated); 275 276 // Get the initial value of each activity stream pref 277 const values = {}; 278 for (const name of this._prefMap.keys()) { 279 values[name] = this._prefs.get(name); 280 } 281 282 // These are not prefs, but are needed to determine stuff in content that can only be 283 // computed in main process 284 values.isPrivateBrowsingEnabled = lazy.PrivateBrowsingUtils.enabled; 285 values.platform = AppConstants.platform; 286 287 // Save the geo pref if we have it 288 if (lazy.Region.home) { 289 values.region = lazy.Region.home; 290 this.geo = values.region; 291 } else if (this.geo !== "") { 292 // Watch for geo changes and use a dummy value for now 293 Services.obs.addObserver(this, lazy.Region.REGION_TOPIC); 294 this.geo = ""; 295 } 296 297 // Get the firefox accounts url for links and to send firstrun metrics to. 298 values.fxa_endpoint = Services.prefs.getStringPref( 299 "browser.newtabpage.activity-stream.fxaccounts.endpoint", 300 "https://accounts.firefox.com" 301 ); 302 303 // Get the firefox update channel with values as default, nightly, beta or release 304 values.appUpdateChannel = Services.prefs.getStringPref( 305 "app.update.channel", 306 "" 307 ); 308 309 // Read the pref for search shortcuts top sites experiment from firefox.js and store it 310 // in our internal list of prefs to watch 311 let searchTopSiteExperimentPrefValue = Services.prefs.getBoolPref( 312 "browser.newtabpage.activity-stream.improvesearch.topSiteSearchShortcuts" 313 ); 314 values["improvesearch.topSiteSearchShortcuts"] = 315 searchTopSiteExperimentPrefValue; 316 this._prefMap.set("improvesearch.topSiteSearchShortcuts", { 317 value: searchTopSiteExperimentPrefValue, 318 }); 319 320 values.mayHaveSponsoredTopSites = Services.prefs.getBoolPref( 321 "browser.topsites.useRemoteSetting" 322 ); 323 324 // Add experiment values and default values 325 values.featureConfig = lazy.NimbusFeatures.newtab.getAllVariables() || {}; 326 values.pocketConfig = 327 lazy.NimbusFeatures.pocketNewtab.getAllVariables() || {}; 328 values.smartShortcutsConfig = 329 lazy.NimbusFeatures.newtabSmartShortcuts.getAllVariables() || {}; 330 values.widgetsConfig = 331 lazy.NimbusFeatures.newtabWidgets.getAllVariables() || {}; 332 values.trainhopConfig = this._getTrainhopConfig(); 333 this._setBoolPref(values, "logowordmark.alwaysVisible", false); 334 this._setBoolPref(values, "feeds.section.topstories", false); 335 this._setBoolPref(values, "discoverystream.enabled", false); 336 this._setBoolPref(values, "discoverystream.hardcoded-basic-layout", false); 337 this._setBoolPref(values, "discoverystream.personalization.enabled", false); 338 this._setBoolPref( 339 values, 340 "discoverystream.personalization.override", 341 false 342 ); 343 this._setStringPref( 344 values, 345 "discoverystream.personalization.modelKeys", 346 "" 347 ); 348 this._setStringPref(values, "discoverystream.spocs-endpoint", ""); 349 this._setStringPref(values, "discoverystream.spocs-endpoint-query", ""); 350 this._setStringPref(values, "newNewtabExperience.colors", ""); 351 this._setBoolPref(values, "search.useHandoffComponent", false); 352 this._setBoolPref(values, "externalComponents.enabled", false); 353 354 // Set the initial state of all prefs in redux 355 this.store.dispatch( 356 ac.BroadcastToContent({ 357 type: at.PREFS_INITIAL_VALUES, 358 data: values, 359 meta: { 360 isStartup: true, 361 }, 362 }) 363 ); 364 } 365 366 uninit() { 367 this.removeListeners(); 368 } 369 370 removeListeners() { 371 this._prefs.ignoreBranch(this); 372 lazy.NimbusFeatures.newtab.offUpdate(this.onExperimentUpdated); 373 lazy.NimbusFeatures.newtabTrainhop.offUpdate( 374 this.onTrainhopExperimentUpdated 375 ); 376 lazy.NimbusFeatures.pocketNewtab.offUpdate(this.onPocketExperimentUpdated); 377 lazy.NimbusFeatures.newtabSmartShortcuts.offUpdate( 378 this.onSmartShortcutsExperimentUpdated 379 ); 380 lazy.NimbusFeatures.newtabInferredPersonalization.offUpdate( 381 this.onInferredPersonalizationExperimentUpdated 382 ); 383 lazy.NimbusFeatures.newtabWidgets.offUpdate(this.onWidgetsUpdated); 384 lazy.NimbusFeatures.newtabOhttpImages.offUpdate(this.onOhttpImagesUpdated); 385 386 if (this.geo === "") { 387 Services.obs.removeObserver(this, lazy.Region.REGION_TOPIC); 388 } 389 } 390 391 observe(subject, topic) { 392 switch (topic) { 393 case lazy.Region.REGION_TOPIC: 394 this.store.dispatch( 395 ac.BroadcastToContent({ 396 type: at.PREF_CHANGED, 397 data: { name: "region", value: lazy.Region.home }, 398 }) 399 ); 400 break; 401 } 402 } 403 404 onAction(action) { 405 switch (action.type) { 406 case at.INIT: 407 this.init(); 408 break; 409 case at.UNINIT: 410 this.uninit(); 411 break; 412 case at.CLEAR_PREF: 413 Services.prefs.clearUserPref(this._prefs._branchStr + action.data.name); 414 break; 415 case at.SET_PREF: 416 this._prefs.set(action.data.name, action.data.value); 417 break; 418 } 419 } 420 }